UNPKG

elation-engine

Version:
1,617 lines (1,356 loc) 246 kB
// Bail in worker threads if (!(typeof Window == 'undefined' || !(window instanceof Window))) { (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.WebVRPolyfill = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){ 'use strict'; var has = Object.prototype.hasOwnProperty , prefix = '~'; /** * Constructor to create a storage for our `EE` objects. * An `Events` instance is a plain object whose properties are event names. * * @constructor * @api private */ function Events() {} // // We try to not inherit from `Object.prototype`. In some engines creating an // instance in this way is faster than calling `Object.create(null)` directly. // If `Object.create(null)` is not supported we prefix the event names with a // character to make sure that the built-in object properties are not // overridden or used as an attack vector. // if (Object.create) { Events.prototype = Object.create(null); // // This hack is needed because the `__proto__` property is still inherited in // some old browsers like Android 4, iPhone 5.1, Opera 11 and Safari 5. // if (!new Events().__proto__) prefix = false; } /** * Representation of a single event listener. * * @param {Function} fn The listener function. * @param {Mixed} context The context to invoke the listener with. * @param {Boolean} [once=false] Specify if the listener is a one-time listener. * @constructor * @api private */ function EE(fn, context, once) { this.fn = fn; this.context = context; this.once = once || false; } /** * Minimal `EventEmitter` interface that is molded against the Node.js * `EventEmitter` interface. * * @constructor * @api public */ function EventEmitter() { this._events = new Events(); this._eventsCount = 0; } /** * Return an array listing the events for which the emitter has registered * listeners. * * @returns {Array} * @api public */ EventEmitter.prototype.eventNames = function eventNames() { var names = [] , events , name; if (this._eventsCount === 0) return names; for (name in (events = this._events)) { if (has.call(events, name)) names.push(prefix ? name.slice(1) : name); } if (Object.getOwnPropertySymbols) { return names.concat(Object.getOwnPropertySymbols(events)); } return names; }; /** * Return the listeners registered for a given event. * * @param {String|Symbol} event The event name. * @param {Boolean} exists Only check if there are listeners. * @returns {Array|Boolean} * @api public */ EventEmitter.prototype.listeners = function listeners(event, exists) { var evt = prefix ? prefix + event : event , available = this._events[evt]; if (exists) return !!available; if (!available) return []; if (available.fn) return [available.fn]; for (var i = 0, l = available.length, ee = new Array(l); i < l; i++) { ee[i] = available[i].fn; } return ee; }; /** * Calls each of the listeners registered for a given event. * * @param {String|Symbol} event The event name. * @returns {Boolean} `true` if the event had listeners, else `false`. * @api public */ EventEmitter.prototype.emit = function emit(event, a1, a2, a3, a4, a5) { var evt = prefix ? prefix + event : event; if (!this._events[evt]) return false; var listeners = this._events[evt] , len = arguments.length , args , i; if (listeners.fn) { if (listeners.once) this.removeListener(event, listeners.fn, undefined, true); switch (len) { case 1: return listeners.fn.call(listeners.context), true; case 2: return listeners.fn.call(listeners.context, a1), true; case 3: return listeners.fn.call(listeners.context, a1, a2), true; case 4: return listeners.fn.call(listeners.context, a1, a2, a3), true; case 5: return listeners.fn.call(listeners.context, a1, a2, a3, a4), true; case 6: return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true; } for (i = 1, args = new Array(len -1); i < len; i++) { args[i - 1] = arguments[i]; } listeners.fn.apply(listeners.context, args); } else { var length = listeners.length , j; for (i = 0; i < length; i++) { if (listeners[i].once) this.removeListener(event, listeners[i].fn, undefined, true); switch (len) { case 1: listeners[i].fn.call(listeners[i].context); break; case 2: listeners[i].fn.call(listeners[i].context, a1); break; case 3: listeners[i].fn.call(listeners[i].context, a1, a2); break; case 4: listeners[i].fn.call(listeners[i].context, a1, a2, a3); break; default: if (!args) for (j = 1, args = new Array(len -1); j < len; j++) { args[j - 1] = arguments[j]; } listeners[i].fn.apply(listeners[i].context, args); } } } return true; }; /** * Add a listener for a given event. * * @param {String|Symbol} event The event name. * @param {Function} fn The listener function. * @param {Mixed} [context=this] The context to invoke the listener with. * @returns {EventEmitter} `this`. * @api public */ EventEmitter.prototype.on = function on(event, fn, context) { var listener = new EE(fn, context || this) , evt = prefix ? prefix + event : event; if (!this._events[evt]) this._events[evt] = listener, this._eventsCount++; else if (!this._events[evt].fn) this._events[evt].push(listener); else this._events[evt] = [this._events[evt], listener]; return this; }; /** * Add a one-time listener for a given event. * * @param {String|Symbol} event The event name. * @param {Function} fn The listener function. * @param {Mixed} [context=this] The context to invoke the listener with. * @returns {EventEmitter} `this`. * @api public */ EventEmitter.prototype.once = function once(event, fn, context) { var listener = new EE(fn, context || this, true) , evt = prefix ? prefix + event : event; if (!this._events[evt]) this._events[evt] = listener, this._eventsCount++; else if (!this._events[evt].fn) this._events[evt].push(listener); else this._events[evt] = [this._events[evt], listener]; return this; }; /** * Remove the listeners of a given event. * * @param {String|Symbol} event The event name. * @param {Function} fn Only remove the listeners that match this function. * @param {Mixed} context Only remove the listeners that have this context. * @param {Boolean} once Only remove one-time listeners. * @returns {EventEmitter} `this`. * @api public */ EventEmitter.prototype.removeListener = function removeListener(event, fn, context, once) { var evt = prefix ? prefix + event : event; if (!this._events[evt]) return this; if (!fn) { if (--this._eventsCount === 0) this._events = new Events(); else delete this._events[evt]; return this; } var listeners = this._events[evt]; if (listeners.fn) { if ( listeners.fn === fn && (!once || listeners.once) && (!context || listeners.context === context) ) { if (--this._eventsCount === 0) this._events = new Events(); else delete this._events[evt]; } } else { for (var i = 0, events = [], length = listeners.length; i < length; i++) { if ( listeners[i].fn !== fn || (once && !listeners[i].once) || (context && listeners[i].context !== context) ) { events.push(listeners[i]); } } // // Reset the array, or remove it completely if we have no more listeners. // if (events.length) this._events[evt] = events.length === 1 ? events[0] : events; else if (--this._eventsCount === 0) this._events = new Events(); else delete this._events[evt]; } return this; }; /** * Remove all listeners, or those of the specified event. * * @param {String|Symbol} [event] The event name. * @returns {EventEmitter} `this`. * @api public */ EventEmitter.prototype.removeAllListeners = function removeAllListeners(event) { var evt; if (event) { evt = prefix ? prefix + event : event; if (this._events[evt]) { if (--this._eventsCount === 0) this._events = new Events(); else delete this._events[evt]; } } else { this._events = new Events(); this._eventsCount = 0; } return this; }; // // Alias methods names because people roll like that. // EventEmitter.prototype.off = EventEmitter.prototype.removeListener; EventEmitter.prototype.addListener = EventEmitter.prototype.on; // // This function doesn't apply anymore. // EventEmitter.prototype.setMaxListeners = function setMaxListeners() { return this; }; // // Expose the prefix. // EventEmitter.prefixed = prefix; // // Allow `EventEmitter` to be imported as module namespace. // EventEmitter.EventEmitter = EventEmitter; // // Expose the module. // if ('undefined' !== typeof module) { module.exports = EventEmitter; } },{}],2:[function(_dereq_,module,exports){ 'use strict'; /* eslint-disable no-unused-vars */ var hasOwnProperty = Object.prototype.hasOwnProperty; var propIsEnumerable = Object.prototype.propertyIsEnumerable; function toObject(val) { if (val === null || val === undefined) { throw new TypeError('Object.assign cannot be called with null or undefined'); } return Object(val); } function shouldUseNative() { try { if (!Object.assign) { return false; } // Detect buggy property enumeration order in older V8 versions. // https://bugs.chromium.org/p/v8/issues/detail?id=4118 var test1 = new String('abc'); // eslint-disable-line test1[5] = 'de'; if (Object.getOwnPropertyNames(test1)[0] === '5') { return false; } // https://bugs.chromium.org/p/v8/issues/detail?id=3056 var test2 = {}; for (var i = 0; i < 10; i++) { test2['_' + String.fromCharCode(i)] = i; } var order2 = Object.getOwnPropertyNames(test2).map(function (n) { return test2[n]; }); if (order2.join('') !== '0123456789') { return false; } // https://bugs.chromium.org/p/v8/issues/detail?id=3056 var test3 = {}; 'abcdefghijklmnopqrst'.split('').forEach(function (letter) { test3[letter] = letter; }); if (Object.keys(Object.assign({}, test3)).join('') !== 'abcdefghijklmnopqrst') { return false; } return true; } catch (e) { // We don't expect any of the above to throw, but better to be safe. return false; } } module.exports = shouldUseNative() ? Object.assign : function (target, source) { var from; var to = toObject(target); var symbols; for (var s = 1; s < arguments.length; s++) { from = Object(arguments[s]); for (var key in from) { if (hasOwnProperty.call(from, key)) { to[key] = from[key]; } } if (Object.getOwnPropertySymbols) { symbols = Object.getOwnPropertySymbols(from); for (var i = 0; i < symbols.length; i++) { if (propIsEnumerable.call(from, symbols[i])) { to[symbols[i]] = from[symbols[i]]; } } } } return to; }; },{}],3:[function(_dereq_,module,exports){ /* * Copyright 2015 Google Inc. All Rights Reserved. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var Util = _dereq_('./util.js'); var WakeLock = _dereq_('./wakelock.js'); // Start at a higher number to reduce chance of conflict. var nextDisplayId = 1000; var hasShowDeprecationWarning = false; var defaultLeftBounds = [0, 0, 0.5, 1]; var defaultRightBounds = [0.5, 0, 0.5, 1]; /** * The base class for all VR frame data. */ function VRFrameData() { this.leftProjectionMatrix = new Float32Array(16); this.leftViewMatrix = new Float32Array(16); this.rightProjectionMatrix = new Float32Array(16); this.rightViewMatrix = new Float32Array(16); this.pose = null; }; /** * The base class for all VR displays. */ function VRDisplay() { this.isPolyfilled = true; this.displayId = nextDisplayId++; this.displayName = 'webvr-polyfill displayName'; this.depthNear = 0.01; this.depthFar = 10000.0; this.isConnected = true; this.isPresenting = false; this.capabilities = { hasPosition: false, hasOrientation: false, hasExternalDisplay: false, canPresent: false, maxLayers: 1 }; this.stageParameters = null; // "Private" members. this.waitingForPresent_ = false; this.layer_ = null; this.fullscreenElement_ = null; this.fullscreenWrapper_ = null; this.fullscreenElementCachedStyle_ = null; this.fullscreenEventTarget_ = null; this.fullscreenChangeHandler_ = null; this.fullscreenErrorHandler_ = null; this.wakelock_ = new WakeLock(); } VRDisplay.prototype.getFrameData = function(frameData) { // TODO: Technically this should retain it's value for the duration of a frame // but I doubt that's practical to do in javascript. return Util.frameDataFromPose(frameData, this.getPose(), this); }; VRDisplay.prototype.getPose = function() { // TODO: Technically this should retain it's value for the duration of a frame // but I doubt that's practical to do in javascript. return this.getImmediatePose(); }; VRDisplay.prototype.requestAnimationFrame = function(callback) { return window.requestAnimationFrame(callback); }; VRDisplay.prototype.cancelAnimationFrame = function(id) { return window.cancelAnimationFrame(id); }; VRDisplay.prototype.wrapForFullscreen = function(element) { // Don't wrap in iOS. if (Util.isIOS()) { return element; } if (!this.fullscreenWrapper_) { this.fullscreenWrapper_ = document.createElement('div'); var cssProperties = [ 'height: ' + Math.min(screen.height, screen.width) + 'px !important', 'top: 0 !important', 'left: 0 !important', 'right: 0 !important', 'border: 0', 'margin: 0', 'padding: 0', 'z-index: 999999 !important', 'position: fixed', ]; this.fullscreenWrapper_.setAttribute('style', cssProperties.join('; ') + ';'); this.fullscreenWrapper_.classList.add('webvr-polyfill-fullscreen-wrapper'); } if (this.fullscreenElement_ == element) { return this.fullscreenWrapper_; } // Remove any previously applied wrappers this.removeFullscreenWrapper(); this.fullscreenElement_ = element; var parent = this.fullscreenElement_.parentElement; parent.insertBefore(this.fullscreenWrapper_, this.fullscreenElement_); parent.removeChild(this.fullscreenElement_); this.fullscreenWrapper_.insertBefore(this.fullscreenElement_, this.fullscreenWrapper_.firstChild); this.fullscreenElementCachedStyle_ = this.fullscreenElement_.getAttribute('style'); var self = this; function applyFullscreenElementStyle() { if (!self.fullscreenElement_) { return; } var cssProperties = [ 'position: absolute', 'top: 0', 'left: 0', 'width: ' + Math.max(screen.width, screen.height) + 'px', 'height: ' + Math.min(screen.height, screen.width) + 'px', 'border: 0', 'margin: 0', 'padding: 0', ]; self.fullscreenElement_.setAttribute('style', cssProperties.join('; ') + ';'); } applyFullscreenElementStyle(); return this.fullscreenWrapper_; }; VRDisplay.prototype.removeFullscreenWrapper = function() { if (!this.fullscreenElement_) { return; } var element = this.fullscreenElement_; if (this.fullscreenElementCachedStyle_) { element.setAttribute('style', this.fullscreenElementCachedStyle_); } else { element.removeAttribute('style'); } this.fullscreenElement_ = null; this.fullscreenElementCachedStyle_ = null; var parent = this.fullscreenWrapper_.parentElement; this.fullscreenWrapper_.removeChild(element); parent.insertBefore(element, this.fullscreenWrapper_); parent.removeChild(this.fullscreenWrapper_); return element; }; VRDisplay.prototype.requestPresent = function(layers) { var wasPresenting = this.isPresenting; var self = this; if (!(layers instanceof Array)) { if (!hasShowDeprecationWarning) { console.warn("Using a deprecated form of requestPresent. Should pass in an array of VRLayers."); hasShowDeprecationWarning = true; } layers = [layers]; } return new Promise(function(resolve, reject) { if (!self.capabilities.canPresent) { reject(new Error('VRDisplay is not capable of presenting.')); return; } if (layers.length == 0 || layers.length > self.capabilities.maxLayers) { reject(new Error('Invalid number of layers.')); return; } var incomingLayer = layers[0]; if (!incomingLayer.source) { /* todo: figure out the correct behavior if the source is not provided. see https://github.com/w3c/webvr/issues/58 */ resolve(); return; } var leftBounds = incomingLayer.leftBounds || defaultLeftBounds; var rightBounds = incomingLayer.rightBounds || defaultRightBounds; if (wasPresenting) { // Already presenting, just changing configuration var layer = self.layer_; if (layer.source !== incomingLayer.source) { layer.source = incomingLayer.source; } for (var i = 0; i < 4; i++) { if (layer.leftBounds[i] !== leftBounds[i]) { layer.leftBounds[i] = leftBounds[i]; } if (layer.rightBounds[i] !== rightBounds[i]) { layer.rightBounds[i] = rightBounds[i]; } } resolve(); return; } // Was not already presenting. self.layer_ = { predistorted: incomingLayer.predistorted, source: incomingLayer.source, leftBounds: leftBounds.slice(0), rightBounds: rightBounds.slice(0) }; self.waitingForPresent_ = false; if (self.layer_ && self.layer_.source) { var fullscreenElement = self.wrapForFullscreen(self.layer_.source); function onFullscreenChange() { var actualFullscreenElement = Util.getFullscreenElement(); self.isPresenting = (fullscreenElement === actualFullscreenElement); if (self.isPresenting) { if (screen.orientation && screen.orientation.lock) { screen.orientation.lock('landscape-primary').catch(function(error){ console.error('screen.orientation.lock() failed due to', error.message) }); } self.waitingForPresent_ = false; self.beginPresent_(); resolve(); } else { if (screen.orientation && screen.orientation.unlock) { screen.orientation.unlock(); } self.removeFullscreenWrapper(); self.wakelock_.release(); self.endPresent_(); self.removeFullscreenListeners_(); } self.fireVRDisplayPresentChange_(); } function onFullscreenError() { if (!self.waitingForPresent_) { return; } self.removeFullscreenWrapper(); self.removeFullscreenListeners_(); self.wakelock_.release(); self.waitingForPresent_ = false; self.isPresenting = false; reject(new Error('Unable to present.')); } self.addFullscreenListeners_(fullscreenElement, onFullscreenChange, onFullscreenError); if (Util.requestFullscreen(fullscreenElement)) { self.wakelock_.request(); self.waitingForPresent_ = true; } else if (Util.isIOS()) { // *sigh* Just fake it. self.wakelock_.request(); self.isPresenting = true; self.beginPresent_(); self.fireVRDisplayPresentChange_(); resolve(); } } if (!self.waitingForPresent_ && !Util.isIOS()) { Util.exitFullscreen(); reject(new Error('Unable to present.')); } }); }; VRDisplay.prototype.exitPresent = function() { var wasPresenting = this.isPresenting; var self = this; this.isPresenting = false; this.layer_ = null; this.wakelock_.release(); return new Promise(function(resolve, reject) { if (wasPresenting) { if (!Util.exitFullscreen() && Util.isIOS()) { self.endPresent_(); self.fireVRDisplayPresentChange_(); } resolve(); } else { reject(new Error('Was not presenting to VRDisplay.')); } }); }; VRDisplay.prototype.getLayers = function() { if (this.layer_) { return [this.layer_]; } return []; }; VRDisplay.prototype.fireVRDisplayPresentChange_ = function() { var event = new CustomEvent('vrdisplaypresentchange', {detail: {display: this}}); window.dispatchEvent(event); }; VRDisplay.prototype.addFullscreenListeners_ = function(element, changeHandler, errorHandler) { this.removeFullscreenListeners_(); this.fullscreenEventTarget_ = element; this.fullscreenChangeHandler_ = changeHandler; this.fullscreenErrorHandler_ = errorHandler; if (changeHandler) { if (document.fullscreenEnabled) { element.addEventListener('fullscreenchange', changeHandler, false); } else if (document.webkitFullscreenEnabled) { element.addEventListener('webkitfullscreenchange', changeHandler, false); } else if (document.mozFullScreenEnabled) { document.addEventListener('mozfullscreenchange', changeHandler, false); } else if (document.msFullscreenEnabled) { element.addEventListener('msfullscreenchange', changeHandler, false); } } if (errorHandler) { if (document.fullscreenEnabled) { element.addEventListener('fullscreenerror', errorHandler, false); } else if (document.webkitFullscreenEnabled) { element.addEventListener('webkitfullscreenerror', errorHandler, false); } else if (document.mozFullScreenEnabled) { document.addEventListener('mozfullscreenerror', errorHandler, false); } else if (document.msFullscreenEnabled) { element.addEventListener('msfullscreenerror', errorHandler, false); } } }; VRDisplay.prototype.removeFullscreenListeners_ = function() { if (!this.fullscreenEventTarget_) return; var element = this.fullscreenEventTarget_; if (this.fullscreenChangeHandler_) { var changeHandler = this.fullscreenChangeHandler_; element.removeEventListener('fullscreenchange', changeHandler, false); element.removeEventListener('webkitfullscreenchange', changeHandler, false); document.removeEventListener('mozfullscreenchange', changeHandler, false); element.removeEventListener('msfullscreenchange', changeHandler, false); } if (this.fullscreenErrorHandler_) { var errorHandler = this.fullscreenErrorHandler_; element.removeEventListener('fullscreenerror', errorHandler, false); element.removeEventListener('webkitfullscreenerror', errorHandler, false); document.removeEventListener('mozfullscreenerror', errorHandler, false); element.removeEventListener('msfullscreenerror', errorHandler, false); } this.fullscreenEventTarget_ = null; this.fullscreenChangeHandler_ = null; this.fullscreenErrorHandler_ = null; }; VRDisplay.prototype.beginPresent_ = function() { // Override to add custom behavior when presentation begins. }; VRDisplay.prototype.endPresent_ = function() { // Override to add custom behavior when presentation ends. }; VRDisplay.prototype.submitFrame = function(pose) { // Override to add custom behavior for frame submission. }; VRDisplay.prototype.getEyeParameters = function(whichEye) { // Override to return accurate eye parameters if canPresent is true. return null; }; /* * Deprecated classes */ /** * The base class for all VR devices. (Deprecated) */ function VRDevice() { this.isPolyfilled = true; this.hardwareUnitId = 'webvr-polyfill hardwareUnitId'; this.deviceId = 'webvr-polyfill deviceId'; this.deviceName = 'webvr-polyfill deviceName'; } /** * The base class for all VR HMD devices. (Deprecated) */ function HMDVRDevice() { } HMDVRDevice.prototype = new VRDevice(); /** * The base class for all VR position sensor devices. (Deprecated) */ function PositionSensorVRDevice() { } PositionSensorVRDevice.prototype = new VRDevice(); module.exports.VRFrameData = VRFrameData; module.exports.VRDisplay = VRDisplay; module.exports.VRDevice = VRDevice; module.exports.HMDVRDevice = HMDVRDevice; module.exports.PositionSensorVRDevice = PositionSensorVRDevice; },{"./util.js":22,"./wakelock.js":24}],4:[function(_dereq_,module,exports){ /* * Copyright 2016 Google Inc. All Rights Reserved. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var CardboardUI = _dereq_('./cardboard-ui.js'); var Util = _dereq_('./util.js'); var WGLUPreserveGLState = _dereq_('./deps/wglu-preserve-state.js'); var distortionVS = [ 'attribute vec2 position;', 'attribute vec3 texCoord;', 'varying vec2 vTexCoord;', 'uniform vec4 viewportOffsetScale[2];', 'void main() {', ' vec4 viewport = viewportOffsetScale[int(texCoord.z)];', ' vTexCoord = (texCoord.xy * viewport.zw) + viewport.xy;', ' gl_Position = vec4( position, 1.0, 1.0 );', '}', ].join('\n'); var distortionFS = [ 'precision mediump float;', 'uniform sampler2D diffuse;', 'varying vec2 vTexCoord;', 'void main() {', ' gl_FragColor = texture2D(diffuse, vTexCoord);', '}', ].join('\n'); /** * A mesh-based distorter. */ function CardboardDistorter(gl) { this.gl = gl; this.ctxAttribs = gl.getContextAttributes(); this.meshWidth = 20; this.meshHeight = 20; this.bufferScale = WebVRConfig.BUFFER_SCALE; this.bufferWidth = gl.drawingBufferWidth; this.bufferHeight = gl.drawingBufferHeight; // Patching support this.realBindFramebuffer = gl.bindFramebuffer; this.realEnable = gl.enable; this.realDisable = gl.disable; this.realColorMask = gl.colorMask; this.realClearColor = gl.clearColor; this.realViewport = gl.viewport; if (!Util.isIOS()) { this.realCanvasWidth = Object.getOwnPropertyDescriptor(gl.canvas.__proto__, 'width'); this.realCanvasHeight = Object.getOwnPropertyDescriptor(gl.canvas.__proto__, 'height'); } this.isPatched = false; // State tracking this.lastBoundFramebuffer = null; this.cullFace = false; this.depthTest = false; this.blend = false; this.scissorTest = false; this.stencilTest = false; this.viewport = [0, 0, 0, 0]; this.colorMask = [true, true, true, true]; this.clearColor = [0, 0, 0, 0]; this.attribs = { position: 0, texCoord: 1 }; this.program = Util.linkProgram(gl, distortionVS, distortionFS, this.attribs); this.uniforms = Util.getProgramUniforms(gl, this.program); this.viewportOffsetScale = new Float32Array(8); this.setTextureBounds(); this.vertexBuffer = gl.createBuffer(); this.indexBuffer = gl.createBuffer(); this.indexCount = 0; this.renderTarget = gl.createTexture(); this.framebuffer = gl.createFramebuffer(); this.depthStencilBuffer = null; this.depthBuffer = null; this.stencilBuffer = null; if (this.ctxAttribs.depth && this.ctxAttribs.stencil) { this.depthStencilBuffer = gl.createRenderbuffer(); } else if (this.ctxAttribs.depth) { this.depthBuffer = gl.createRenderbuffer(); } else if (this.ctxAttribs.stencil) { this.stencilBuffer = gl.createRenderbuffer(); } this.patch(); this.onResize(); if (!WebVRConfig.CARDBOARD_UI_DISABLED) { this.cardboardUI = new CardboardUI(gl); } }; /** * Tears down all the resources created by the distorter and removes any * patches. */ CardboardDistorter.prototype.destroy = function() { var gl = this.gl; this.unpatch(); gl.deleteProgram(this.program); gl.deleteBuffer(this.vertexBuffer); gl.deleteBuffer(this.indexBuffer); gl.deleteTexture(this.renderTarget); gl.deleteFramebuffer(this.framebuffer); if (this.depthStencilBuffer) { gl.deleteRenderbuffer(this.depthStencilBuffer); } if (this.depthBuffer) { gl.deleteRenderbuffer(this.depthBuffer); } if (this.stencilBuffer) { gl.deleteRenderbuffer(this.stencilBuffer); } if (this.cardboardUI) { this.cardboardUI.destroy(); } }; /** * Resizes the backbuffer to match the canvas width and height. */ CardboardDistorter.prototype.onResize = function() { var gl = this.gl; var self = this; var glState = [ gl.RENDERBUFFER_BINDING, gl.TEXTURE_BINDING_2D, gl.TEXTURE0 ]; WGLUPreserveGLState(gl, glState, function(gl) { // Bind real backbuffer and clear it once. We don't need to clear it again // after that because we're overwriting the same area every frame. self.realBindFramebuffer.call(gl, gl.FRAMEBUFFER, null); // Put things in a good state if (self.scissorTest) { self.realDisable.call(gl, gl.SCISSOR_TEST); } self.realColorMask.call(gl, true, true, true, true); self.realViewport.call(gl, 0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); self.realClearColor.call(gl, 0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); // Now bind and resize the fake backbuffer self.realBindFramebuffer.call(gl, gl.FRAMEBUFFER, self.framebuffer); gl.bindTexture(gl.TEXTURE_2D, self.renderTarget); gl.texImage2D(gl.TEXTURE_2D, 0, self.ctxAttribs.alpha ? gl.RGBA : gl.RGB, self.bufferWidth, self.bufferHeight, 0, self.ctxAttribs.alpha ? gl.RGBA : gl.RGB, gl.UNSIGNED_BYTE, null); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, self.renderTarget, 0); if (self.ctxAttribs.depth && self.ctxAttribs.stencil) { gl.bindRenderbuffer(gl.RENDERBUFFER, self.depthStencilBuffer); gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_STENCIL, self.bufferWidth, self.bufferHeight); gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, self.depthStencilBuffer); } else if (self.ctxAttribs.depth) { gl.bindRenderbuffer(gl.RENDERBUFFER, self.depthBuffer); gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, self.bufferWidth, self.bufferHeight); gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, self.depthBuffer); } else if (self.ctxAttribs.stencil) { gl.bindRenderbuffer(gl.RENDERBUFFER, self.stencilBuffer); gl.renderbufferStorage(gl.RENDERBUFFER, gl.STENCIL_INDEX8, self.bufferWidth, self.bufferHeight); gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.STENCIL_ATTACHMENT, gl.RENDERBUFFER, self.stencilBuffer); } if (!gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE) { console.error('Framebuffer incomplete!'); } self.realBindFramebuffer.call(gl, gl.FRAMEBUFFER, self.lastBoundFramebuffer); if (self.scissorTest) { self.realEnable.call(gl, gl.SCISSOR_TEST); } self.realColorMask.apply(gl, self.colorMask); self.realViewport.apply(gl, self.viewport); self.realClearColor.apply(gl, self.clearColor); }); if (this.cardboardUI) { this.cardboardUI.onResize(); } }; CardboardDistorter.prototype.patch = function() { if (this.isPatched) { return; } var self = this; var canvas = this.gl.canvas; var gl = this.gl; if (!Util.isIOS()) { canvas.width = Util.getScreenWidth() * this.bufferScale; canvas.height = Util.getScreenHeight() * this.bufferScale; Object.defineProperty(canvas, 'width', { configurable: true, enumerable: true, get: function() { return self.bufferWidth; }, set: function(value) { self.bufferWidth = value; self.realCanvasWidth.set.call(canvas, value); self.onResize(); } }); Object.defineProperty(canvas, 'height', { configurable: true, enumerable: true, get: function() { return self.bufferHeight; }, set: function(value) { self.bufferHeight = value; self.realCanvasHeight.set.call(canvas, value); self.onResize(); } }); } this.lastBoundFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING); if (this.lastBoundFramebuffer == null) { this.lastBoundFramebuffer = this.framebuffer; this.gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); } this.gl.bindFramebuffer = function(target, framebuffer) { self.lastBoundFramebuffer = framebuffer ? framebuffer : self.framebuffer; // Silently make calls to bind the default framebuffer bind ours instead. self.realBindFramebuffer.call(gl, target, self.lastBoundFramebuffer); }; this.cullFace = gl.getParameter(gl.CULL_FACE); this.depthTest = gl.getParameter(gl.DEPTH_TEST); this.blend = gl.getParameter(gl.BLEND); this.scissorTest = gl.getParameter(gl.SCISSOR_TEST); this.stencilTest = gl.getParameter(gl.STENCIL_TEST); gl.enable = function(pname) { switch (pname) { case gl.CULL_FACE: self.cullFace = true; break; case gl.DEPTH_TEST: self.depthTest = true; break; case gl.BLEND: self.blend = true; break; case gl.SCISSOR_TEST: self.scissorTest = true; break; case gl.STENCIL_TEST: self.stencilTest = true; break; } self.realEnable.call(gl, pname); }; gl.disable = function(pname) { switch (pname) { case gl.CULL_FACE: self.cullFace = false; break; case gl.DEPTH_TEST: self.depthTest = false; break; case gl.BLEND: self.blend = false; break; case gl.SCISSOR_TEST: self.scissorTest = false; break; case gl.STENCIL_TEST: self.stencilTest = false; break; } self.realDisable.call(gl, pname); }; this.colorMask = gl.getParameter(gl.COLOR_WRITEMASK); gl.colorMask = function(r, g, b, a) { self.colorMask[0] = r; self.colorMask[1] = g; self.colorMask[2] = b; self.colorMask[3] = a; self.realColorMask.call(gl, r, g, b, a); }; this.clearColor = gl.getParameter(gl.COLOR_CLEAR_VALUE); gl.clearColor = function(r, g, b, a) { self.clearColor[0] = r; self.clearColor[1] = g; self.clearColor[2] = b; self.clearColor[3] = a; self.realClearColor.call(gl, r, g, b, a); }; this.viewport = gl.getParameter(gl.VIEWPORT); gl.viewport = function(x, y, w, h) { self.viewport[0] = x; self.viewport[1] = y; self.viewport[2] = w; self.viewport[3] = h; self.realViewport.call(gl, x, y, w, h); }; this.isPatched = true; Util.safariCssSizeWorkaround(canvas); }; CardboardDistorter.prototype.unpatch = function() { if (!this.isPatched) { return; } var gl = this.gl; var canvas = this.gl.canvas; if (!Util.isIOS()) { Object.defineProperty(canvas, 'width', this.realCanvasWidth); Object.defineProperty(canvas, 'height', this.realCanvasHeight); } canvas.width = this.bufferWidth; canvas.height = this.bufferHeight; gl.bindFramebuffer = this.realBindFramebuffer; gl.enable = this.realEnable; gl.disable = this.realDisable; gl.colorMask = this.realColorMask; gl.clearColor = this.realClearColor; gl.viewport = this.realViewport; // Check to see if our fake backbuffer is bound and bind the real backbuffer // if that's the case. if (this.lastBoundFramebuffer == this.framebuffer) { gl.bindFramebuffer(gl.FRAMEBUFFER, null); } this.isPatched = false; setTimeout(function() { Util.safariCssSizeWorkaround(canvas); }, 1); }; CardboardDistorter.prototype.setTextureBounds = function(leftBounds, rightBounds) { if (!leftBounds) { leftBounds = [0, 0, 0.5, 1]; } if (!rightBounds) { rightBounds = [0.5, 0, 0.5, 1]; } // Left eye this.viewportOffsetScale[0] = leftBounds[0]; // X this.viewportOffsetScale[1] = leftBounds[1]; // Y this.viewportOffsetScale[2] = leftBounds[2]; // Width this.viewportOffsetScale[3] = leftBounds[3]; // Height // Right eye this.viewportOffsetScale[4] = rightBounds[0]; // X this.viewportOffsetScale[5] = rightBounds[1]; // Y this.viewportOffsetScale[6] = rightBounds[2]; // Width this.viewportOffsetScale[7] = rightBounds[3]; // Height }; /** * Performs distortion pass on the injected backbuffer, rendering it to the real * backbuffer. */ CardboardDistorter.prototype.submitFrame = function() { var gl = this.gl; var self = this; var glState = []; if (!WebVRConfig.DIRTY_SUBMIT_FRAME_BINDINGS) { glState.push( gl.CURRENT_PROGRAM, gl.ARRAY_BUFFER_BINDING, gl.ELEMENT_ARRAY_BUFFER_BINDING, gl.TEXTURE_BINDING_2D, gl.TEXTURE0 ); } WGLUPreserveGLState(gl, glState, function(gl) { // Bind the real default framebuffer self.realBindFramebuffer.call(gl, gl.FRAMEBUFFER, null); // Make sure the GL state is in a good place if (self.cullFace) { self.realDisable.call(gl, gl.CULL_FACE); } if (self.depthTest) { self.realDisable.call(gl, gl.DEPTH_TEST); } if (self.blend) { self.realDisable.call(gl, gl.BLEND); } if (self.scissorTest) { self.realDisable.call(gl, gl.SCISSOR_TEST); } if (self.stencilTest) { self.realDisable.call(gl, gl.STENCIL_TEST); } self.realColorMask.call(gl, true, true, true, true); self.realViewport.call(gl, 0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); // If the backbuffer has an alpha channel clear every frame so the page // doesn't show through. if (self.ctxAttribs.alpha || Util.isIOS()) { self.realClearColor.call(gl, 0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); } // Bind distortion program and mesh gl.useProgram(self.program); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, self.indexBuffer); gl.bindBuffer(gl.ARRAY_BUFFER, self.vertexBuffer); gl.enableVertexAttribArray(self.attribs.position); gl.enableVertexAttribArray(self.attribs.texCoord); gl.vertexAttribPointer(self.attribs.position, 2, gl.FLOAT, false, 20, 0); gl.vertexAttribPointer(self.attribs.texCoord, 3, gl.FLOAT, false, 20, 8); gl.activeTexture(gl.TEXTURE0); gl.uniform1i(self.uniforms.diffuse, 0); gl.bindTexture(gl.TEXTURE_2D, self.renderTarget); gl.uniform4fv(self.uniforms.viewportOffsetScale, self.viewportOffsetScale); // Draws both eyes gl.drawElements(gl.TRIANGLES, self.indexCount, gl.UNSIGNED_SHORT, 0); if (self.cardboardUI) { self.cardboardUI.renderNoState(); } // Bind the fake default framebuffer again self.realBindFramebuffer.call(self.gl, gl.FRAMEBUFFER, self.framebuffer); // If preserveDrawingBuffer == false clear the framebuffer if (!self.ctxAttribs.preserveDrawingBuffer) { self.realClearColor.call(gl, 0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT); } if (!WebVRConfig.DIRTY_SUBMIT_FRAME_BINDINGS) { self.realBindFramebuffer.call(gl, gl.FRAMEBUFFER, self.lastBoundFramebuffer); } // Restore state if (self.cullFace) { self.realEnable.call(gl, gl.CULL_FACE); } if (self.depthTest) { self.realEnable.call(gl, gl.DEPTH_TEST); } if (self.blend) { self.realEnable.call(gl, gl.BLEND); } if (self.scissorTest) { self.realEnable.call(gl, gl.SCISSOR_TEST); } if (self.stencilTest) { self.realEnable.call(gl, gl.STENCIL_TEST); } self.realColorMask.apply(gl, self.colorMask); self.realViewport.apply(gl, self.viewport); if (self.ctxAttribs.alpha || !self.ctxAttribs.preserveDrawingBuffer) { self.realClearColor.apply(gl, self.clearColor); } }); // Workaround for the fact that Safari doesn't allow us to patch the canvas // width and height correctly. After each submit frame check to see what the // real backbuffer size has been set to and resize the fake backbuffer size // to match. if (Util.isIOS()) { var canvas = gl.canvas; if (canvas.width != self.bufferWidth || canvas.height != self.bufferHeight) { self.bufferWidth = canvas.width; self.bufferHeight = canvas.height; self.onResize(); } } }; /** * Call when the deviceInfo has changed. At this point we need * to re-calculate the distortion mesh. */ CardboardDistorter.prototype.updateDeviceInfo = function(deviceInfo) { var gl = this.gl; var self = this; var glState = [gl.ARRAY_BUFFER_BINDING, gl.ELEMENT_ARRAY_BUFFER_BINDING]; WGLUPreserveGLState(gl, glState, function(gl) { var vertices = self.computeMeshVertices_(self.meshWidth, self.meshHeight, deviceInfo); gl.bindBuffer(gl.ARRAY_BUFFER, self.vertexBuffer); gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); // Indices don't change based on device parameters, so only compute once. if (!self.indexCount) { var indices = self.computeMeshIndices_(self.meshWidth, self.meshHeight); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, self.indexBuffer); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW); self.indexCount = indices.length; } }); }; /** * Build the distortion mesh vertices. * Based on code from the Unity cardboard plugin. */ CardboardDistorter.prototype.computeMeshVertices_ = function(width, height, deviceInfo) { var vertices = new Float32Array(2 * width * height * 5); var lensFrustum = deviceInfo.getLeftEyeVisibleTanAngles(); var noLensFrustum = deviceInfo.getLeftEyeNoLensTanAngles(); var viewport = deviceInfo.getLeftEyeVisibleScreenRect(noLensFrustum); var vidx = 0; var iidx = 0; for (var e = 0; e < 2; e++) { for (var j = 0; j < height; j++) { for (var i = 0; i < width; i++, vidx++) { var u = i / (width - 1); var v = j / (height - 1); // Grid points regularly spaced in StreoScreen, and barrel distorted in // the mesh. var s = u; var t = v; var x = Util.lerp(lensFrustum[0], lensFrustum[2], u); var y = Util.lerp(lensFrustum[3], lensFrustum[1], v); var d = Math.sqrt(x * x + y * y); var r = deviceInfo.distortion.distortInverse(d); var p = x * r / d; var q = y * r / d; u = (p - noLensFrustum[0]) / (noLensFrustum[2] - noLensFrustum[0]); v = (q - noLensFrustum[3]) / (noLensFrustum[1] - noLensFrustum[3]); // Convert u,v to mesh screen coordinates. var aspect = deviceInfo.device.widthMeters / deviceInfo.device.heightMeters; // FIXME: The original Unity plugin multiplied U by the aspect ratio // and didn't multiply either value by 2, but that seems to get it // really close to correct looking for me. I hate this kind of "Don't // know why it works" code though, and wold love a more logical // explanation of what needs to happen here. u = (viewport.x + u * viewport.width - 0.5) * 2.0; //* aspect; v = (viewport.y + v * viewport.height - 0.5) * 2.0; vertices[(vidx * 5) + 0] = u; // position.x vertices[(vidx * 5) + 1] = v; // position.y vertices[(vidx * 5) + 2] = s; // texCoord.x vertices[(vidx * 5) + 3] = t; // texCoord.y vertices[(vidx * 5) + 4] = e; // texCoord.z (viewport index) } } var w = lensFrustum[2] - lensFrustum[0]; lensFrustum[0] = -(w + lensFrustum[0]); lensFrustum[2] = w - lensFrustum[2]; w = noLensFrustum[2] - noLensFrustum[0]; noLensFrustum[0] = -(w + noLensFrustum[0]); noLensFrustum[2] = w - noLensFrustum[2]; viewport.x = 1 - (viewport.x + viewport.width); } return vertices; } /** * Build the distortion mesh indices. * Based on code from the Unity cardboard plugin. */ CardboardDistorter.prototype.computeMeshIndices_ = function(width, height) { var indices = new Uint16Array(2 * (width - 1) * (height - 1) * 6); var halfwidth = width / 2; var halfheight = height / 2; var vidx = 0; var iidx = 0; for (var e = 0; e < 2; e++) { for (var j = 0; j < height; j++) { for (var i = 0; i < width; i++, vidx++) { if (i == 0 || j == 0) continue; // Build a quad. Lower right and upper left quadrants have quads with // the triangle diagonal flipped to get the vignette to interpolate // correctly. if ((i <= halfwidth) == (j <= halfheight)) { // Quad diagonal lower left to upper right. indices[iidx++] = vidx; indices[iidx++] = vidx - width - 1; indices[iidx++] = vidx - width; indices[iidx++] = vidx - width - 1; indices[iidx++] = vidx; indices[iidx++] = vidx - 1; } else { // Quad diagonal upper left to lower right. indices[iidx++] = vidx - 1; indices[iidx++] = vidx - width; indices[iidx++] = vidx; indices[iidx++] = vidx - width; indices[iidx++] = vidx - 1; indices[iidx++] = vidx - width - 1; } } } } return indices; }; CardboardDistorter.prototype.getOwnPropertyDescriptor_ = function(proto, attrName) { var descriptor = Object.getOwnPropertyDescriptor(proto, attrName); // In some cases (ahem... Safari), the descriptor returns undefined get and // set fields. In this case, we need to create a synthetic property // descriptor. This works around some of the issues in // https://github.com/borismus/webvr-polyfill/issues/46 if (descriptor.get === undefined || descriptor.set === undefined) { descriptor.configurable = true; descriptor.enumerable = true; descriptor.get = function() { return this.getAttribute(attrName); }; descriptor.set = function(val) { this.setAttribute(attrName, val); }; } return descriptor; }; module.exports = CardboardDistorter; },{"./cardboard-ui.js":5,"./deps/wglu-preserve-state.js":7,"./util.js":22}],5:[function(_dereq_,module,exports){ /* * Copyright 2016 Google Inc. All Rights Reserved. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var Util = _dereq_('./util.js'); var WGLUPreserveGLState = _dereq_('./deps/wglu-preserve-state.js'); var uiVS = [ 'attribute vec2 position;', 'uniform mat4 projectionMat;', 'void main() {', ' gl_Position = projectionMat * vec4( position, -1.0, 1.0 );', '}', ].join('\n'); var uiFS = [ 'precision mediump float;', 'uniform vec4 color;', 'void main() {', ' gl_FragColor = color;', '}', ].join('\n'); var DEG2RAD = Math.PI/180.0; // The gear has 6 identical sections, each spanning 60 degrees. var kAnglePerGearSection = 60; // Half-angle of the span of the outer rim. var kOuterRimEndAngle = 12; // Angle between the middle of the outer rim and the start of the inner rim. var kInnerRimBeginAngle = 20; // Distance from center to outer rim, normalized so that the entire model // fits in a [-1, 1] x [-1, 1] square. var kOuterRadius = 1; // Distance from center to depressed rim, in model units. var kMiddleRadius = 0.75; // Radius of the inner hollow circle, in model units. var kInnerRadius = 0.3125; // Center line thickness in DP. var kCenterLineThicknessDp = 4; // Button width in DP. var kButtonWidthDp = 28; // Factor to scale the touch area that responds to the touch. var kTouchSlopFactor = 1.5; var Angles = [ 0, kOuterRimEndAngle, kInnerRimBeginAngle, kAnglePerGearSection - kInnerRimBeginAngle, kAnglePerGearSection - kOuterRimEndAngle ]; /** * Renders the alignment line and "options" gear. It is assumed that the canvas * this is rendered into covers the entire screen (or close to it.) */ function CardboardUI(gl) { this.gl = gl; this.attribs = { position: 0 }; this.program = Util.linkProgram(gl, uiVS, uiFS, this.attribs); this.uniforms = Util.getProgramUniforms(gl, this.program); this.vertexBuffer = gl.createBuffer(); this.gearOffset = 0; this.gearVertexCount = 0; this.arrowOffset = 0; this.arrowVertexCount = 0; this.projMat = new Float32Array(16); this.listener = null; this.onResize(); }; /** * Tears down all the resources created by the UI renderer. */ CardboardUI.prototype.destroy = function() { var gl = this.gl; if (this.listener) { gl.canvas.removeEventListener('click', this.listener, false); } gl.deleteProgram(this.program); gl.deleteBuffer(this.vertexBuffer); }; /** * Adds a listene