diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..a02d6c2 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +### Name and Project Name +Luke Carlson - Threejs project +### Link to GitHub Pages site +[right here](http://pennvr.github.io/three-js-jlukec) +### Techniques used, and why those techniques +For this assignment I built a version of the [diamond square algorithm](https://en.wikipedia.org/wiki/Diamond-square_algorithm) to generate my terrain from a PlaneGeometry. It seemed like a reasonable way to generate the terrain. You can see the code at js/diamond_square.js + +For my particle system, I created a bunch of small red spheres that explode in random directions. + +For controls, I used code from the PointerLockControl & webvr cube THREE.js examples. Those examples were super helpful for setting up my environment & world. +That way I could test both with a headset in VR and on a laptop with the mouse +### Instructions on building/assembling/etc. Also document how to run the code +No assembly required, just load the index.html page (which will load my js/diamond_square.js algorithm and the external libs). + +Note: it may take a moment to generate the terrain at the start +### When in VR mode, did you feel any motion sickness? Why and why not? +No, I think it is because there isn't much movement going on in the scene so there isn't an opportunity to get motion sickness +### What was the hardest part of the assignment? +A lot of it was getting used to Threejs and the small quirks, like learning that number of vertices in a plane is (segments+1) * (segments+1) + +I'd say the terrain generation algorithm took the longest. I originally went with some sort of smooth randomness but that didn't look as cool. +### What do you wish you’d done differently? +Now that I've finished the project I feel like I understand threejs a lot better so if I did the project again I would +take it in a different direction and try to generate better terrain, trees, or look into flowing water. + +There is a bug in the square section of DS that leads to some vertices not getting averaged correctly which is unfortunate and if I had a bit more time I'd fix that. +### What do you wish we had done differently? +Not much, the difficulty in the project was mainly getting into threejs. You guys gave us extra time which was really helpful. I think it would be interesting to try to do a similar assignment again at the end of the semester and see how we progress diff --git a/index.html b/index.html new file mode 100644 index 0000000..5a541fe --- /dev/null +++ b/index.html @@ -0,0 +1,422 @@ + + + + + Luke three.js + + + + + + + + + + + + + + +
+ +
+ Click to play +
(W, A, S, D = Move, SPACE = Jump, MOUSE = Look around) +
+ +
+ + + + + diff --git a/js/diamond_square.js b/js/diamond_square.js new file mode 100644 index 0000000..c83f5bc --- /dev/null +++ b/js/diamond_square.js @@ -0,0 +1,44 @@ +// my terrain generation algorithm +// The inputs are side_length (the plane must have sides whose length is of the form 2^n -1), +// magnitude (sets the bounds on the randomness), +// reduction_scale (how fast the algorithm 'levels out') +var diamondSquare = function diamondSquare(side_length, magnitude, reduction_scale) { + // think this is how you do 2d arrays in javascript + arr = new Array(side_length); + for (var i = 0; i < side_length; i++) { + arr[i] = new Float32Array(side_length); + } + // initialize the sides + arr[0][0] = Math.random() * magnitude * 2 - magnitude; + arr[0][side_length-1] = Math.random() * magnitude * 2 - magnitude; + arr[side_length-1][0] = Math.random() * magnitude * 2 - magnitude; + arr[side_length-1][side_length-1] = Math.random() * magnitude- magnitude; + + function dsHelper(top_i,top_j,sub_length,m,s) { + if (sub_length <= 0) { + return; + } + var half_sub_length = Math.floor(sub_length/2); + + // DIAMOND + var corners = [ + arr[top_i][top_j], + arr[top_i + sub_length][top_j], + arr[top_i][top_j + sub_length], + arr[top_i + sub_length][top_j + sub_length] + ]; + var avg = corners.reduce(function(a,b) {return a+b;}) / 4; + arr[top_i + half_sub_length][top_j + half_sub_length] = avg + Math.random() * m; + + + + dsHelper(top_i,top_j, half_sub_length, m*s,s); + dsHelper(top_i,top_j + half_sub_length, half_sub_length,m*s,s); + dsHelper(top_i + half_sub_length,top_j, half_sub_length,m*s,s); + dsHelper(top_i + half_sub_length,top_j + half_sub_length, half_sub_length,m*s,s); + } + dsHelper(0,0,side_length-1,magnitude,reduction_scale); + + + return arr; +} diff --git a/js/lib/PointerLockControls.js b/js/lib/PointerLockControls.js new file mode 100644 index 0000000..b0d7dc8 --- /dev/null +++ b/js/lib/PointerLockControls.js @@ -0,0 +1,69 @@ +/** + * @author mrdoob / http://mrdoob.com/ + */ + +THREE.PointerLockControls = function ( camera ) { + + var scope = this; + + camera.rotation.set( 0, 0, 0 ); + + var pitchObject = new THREE.Object3D(); + pitchObject.add( camera ); + + var yawObject = new THREE.Object3D(); + yawObject.position.y = 10; + yawObject.add( pitchObject ); + + var PI_2 = Math.PI / 2; + + var onMouseMove = function ( event ) { + + if ( scope.enabled === false ) return; + + var movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0; + var movementY = event.movementY || event.mozMovementY || event.webkitMovementY || 0; + + yawObject.rotation.y -= movementX * 0.002; + pitchObject.rotation.x -= movementY * 0.002; + + pitchObject.rotation.x = Math.max( - PI_2, Math.min( PI_2, pitchObject.rotation.x ) ); + + }; + + this.dispose = function() { + + document.removeEventListener( 'mousemove', onMouseMove, false ); + + }; + + document.addEventListener( 'mousemove', onMouseMove, false ); + + this.enabled = false; + + this.getObject = function () { + + return yawObject; + + }; + + this.getDirection = function() { + + // assumes the camera itself is not rotated + + var direction = new THREE.Vector3( 0, 0, - 1 ); + var rotation = new THREE.Euler( 0, 0, 0, "YXZ" ); + + return function( v ) { + + rotation.set( pitchObject.rotation.x, yawObject.rotation.y, 0 ); + + v.copy( direction ).applyEuler( rotation ); + + return v; + + }; + + }(); + +}; diff --git a/js/lib/VRControls.js b/js/lib/VRControls.js new file mode 100644 index 0000000..6eecd60 --- /dev/null +++ b/js/lib/VRControls.js @@ -0,0 +1,173 @@ +/** + * @author dmarcos / https://github.com/dmarcos + * @author mrdoob / http://mrdoob.com + */ + +THREE.VRControls = function ( object, onError ) { + + var scope = this; + + var vrDisplay, vrDisplays; + + var standingMatrix = new THREE.Matrix4(); + + var frameData = null; + + if ( 'VRFrameData' in window ) { + + frameData = new VRFrameData(); + + } + + function gotVRDisplays( displays ) { + + vrDisplays = displays; + + if ( displays.length > 0 ) { + + vrDisplay = displays[ 0 ]; + + } else { + + if ( onError ) onError( 'VR input not available.' ); + + } + + } + + if ( navigator.getVRDisplays ) { + + navigator.getVRDisplays().then( gotVRDisplays ).catch ( function () { + + console.warn( 'THREE.VRControls: Unable to get VR Displays' ); + + } ); + + } + + // the Rift SDK returns the position in meters + // this scale factor allows the user to define how meters + // are converted to scene units. + + this.scale = 1; + + // If true will use "standing space" coordinate system where y=0 is the + // floor and x=0, z=0 is the center of the room. + this.standing = false; + + // Distance from the users eyes to the floor in meters. Used when + // standing=true but the VRDisplay doesn't provide stageParameters. + this.userHeight = 1.6; + + this.getVRDisplay = function () { + + return vrDisplay; + + }; + + this.setVRDisplay = function ( value ) { + + vrDisplay = value; + + }; + + this.getVRDisplays = function () { + + console.warn( 'THREE.VRControls: getVRDisplays() is being deprecated.' ); + return vrDisplays; + + }; + + this.getStandingMatrix = function () { + + return standingMatrix; + + }; + + this.update = function () { + + if ( vrDisplay ) { + + var pose; + + if ( vrDisplay.getFrameData ) { + + vrDisplay.getFrameData( frameData ); + pose = frameData.pose; + + } else if ( vrDisplay.getPose ) { + + pose = vrDisplay.getPose(); + + } + + if ( pose.orientation !== null ) { + + object.quaternion.fromArray( pose.orientation ); + + } + + if ( pose.position !== null ) { + + object.position.fromArray( pose.position ); + + } else { + + object.position.set( 0, 0, 0 ); + + } + + if ( this.standing ) { + + if ( vrDisplay.stageParameters ) { + + object.updateMatrix(); + + standingMatrix.fromArray( vrDisplay.stageParameters.sittingToStandingTransform ); + object.applyMatrix( standingMatrix ); + + } else { + + object.position.setY( object.position.y + this.userHeight ); + + } + + } + + object.position.multiplyScalar( scope.scale ); + + } + + }; + + this.resetPose = function () { + + if ( vrDisplay ) { + + vrDisplay.resetPose(); + + } + + }; + + this.resetSensor = function () { + + console.warn( 'THREE.VRControls: .resetSensor() is now .resetPose().' ); + this.resetPose(); + + }; + + this.zeroSensor = function () { + + console.warn( 'THREE.VRControls: .zeroSensor() is now .resetPose().' ); + this.resetPose(); + + }; + + this.dispose = function () { + + vrDisplay = null; + + }; + +}; diff --git a/js/lib/VREffect.js b/js/lib/VREffect.js new file mode 100644 index 0000000..5911ea1 --- /dev/null +++ b/js/lib/VREffect.js @@ -0,0 +1,477 @@ +/** + * @author dmarcos / https://github.com/dmarcos + * @author mrdoob / http://mrdoob.com + * + * WebVR Spec: http://mozvr.github.io/webvr-spec/webvr.html + * + * Firefox: http://mozvr.com/downloads/ + * Chromium: https://webvr.info/get-chrome + * + */ + +THREE.VREffect = function( renderer, onError ) { + + var vrDisplay, vrDisplays; + var eyeTranslationL = new THREE.Vector3(); + var eyeTranslationR = new THREE.Vector3(); + var renderRectL, renderRectR; + + var frameData = null; + + if ( 'VRFrameData' in window ) { + + frameData = new window.VRFrameData(); + + } + + function gotVRDisplays( displays ) { + + vrDisplays = displays; + + if ( displays.length > 0 ) { + + vrDisplay = displays[ 0 ]; + + } else { + + if ( onError ) onError( 'HMD not available' ); + + } + + } + + if ( navigator.getVRDisplays ) { + + navigator.getVRDisplays().then( gotVRDisplays ).catch( function() { + + console.warn( 'THREE.VREffect: Unable to get VR Displays' ); + + } ); + + } + + // + + this.isPresenting = false; + this.scale = 1; + + var scope = this; + + var rendererSize = renderer.getSize(); + var rendererUpdateStyle = false; + var rendererPixelRatio = renderer.getPixelRatio(); + + this.getVRDisplay = function() { + + return vrDisplay; + + }; + + this.setVRDisplay = function( value ) { + + vrDisplay = value; + + }; + + this.getVRDisplays = function() { + + console.warn( 'THREE.VREffect: getVRDisplays() is being deprecated.' ); + return vrDisplays; + + }; + + this.setSize = function( width, height, updateStyle ) { + + rendererSize = { width: width, height: height }; + rendererUpdateStyle = updateStyle; + + if ( scope.isPresenting ) { + + var eyeParamsL = vrDisplay.getEyeParameters( 'left' ); + renderer.setPixelRatio( 1 ); + renderer.setSize( eyeParamsL.renderWidth * 2, eyeParamsL.renderHeight, false ); + + } else { + + renderer.setPixelRatio( rendererPixelRatio ); + renderer.setSize( width, height, updateStyle ); + + } + + }; + + // VR presentation + + var canvas = renderer.domElement; + var defaultLeftBounds = [ 0.0, 0.0, 0.5, 1.0 ]; + var defaultRightBounds = [ 0.5, 0.0, 0.5, 1.0 ]; + + function onVRDisplayPresentChange() { + + var wasPresenting = scope.isPresenting; + scope.isPresenting = vrDisplay !== undefined && vrDisplay.isPresenting; + + if ( scope.isPresenting ) { + + var eyeParamsL = vrDisplay.getEyeParameters( 'left' ); + var eyeWidth = eyeParamsL.renderWidth; + var eyeHeight = eyeParamsL.renderHeight; + + if ( ! wasPresenting ) { + + rendererPixelRatio = renderer.getPixelRatio(); + rendererSize = renderer.getSize(); + + renderer.setPixelRatio( 1 ); + renderer.setSize( eyeWidth * 2, eyeHeight, false ); + + } + + } else if ( wasPresenting ) { + + renderer.setPixelRatio( rendererPixelRatio ); + renderer.setSize( rendererSize.width, rendererSize.height, rendererUpdateStyle ); + + } + + } + + window.addEventListener( 'vrdisplaypresentchange', onVRDisplayPresentChange, false ); + + this.setFullScreen = function( boolean ) { + + return new Promise( function( resolve, reject ) { + + if ( vrDisplay === undefined ) { + + reject( new Error( 'No VR hardware found.' ) ); + return; + + } + + if ( scope.isPresenting === boolean ) { + + resolve(); + return; + + } + + if ( boolean ) { + + resolve( vrDisplay.requestPresent( [ { source: canvas } ] ) ); + + } else { + + resolve( vrDisplay.exitPresent() ); + + } + + } ); + + }; + + this.requestPresent = function() { + + return this.setFullScreen( true ); + + }; + + this.exitPresent = function() { + + return this.setFullScreen( false ); + + }; + + this.requestAnimationFrame = function( f ) { + + if ( vrDisplay !== undefined ) { + + return vrDisplay.requestAnimationFrame( f ); + + } else { + + return window.requestAnimationFrame( f ); + + } + + }; + + this.cancelAnimationFrame = function( h ) { + + if ( vrDisplay !== undefined ) { + + vrDisplay.cancelAnimationFrame( h ); + + } else { + + window.cancelAnimationFrame( h ); + + } + + }; + + this.submitFrame = function() { + + if ( vrDisplay !== undefined && scope.isPresenting ) { + + vrDisplay.submitFrame(); + + } + + }; + + this.autoSubmitFrame = true; + + // render + + var cameraL = new THREE.PerspectiveCamera(); + cameraL.layers.enable( 1 ); + + var cameraR = new THREE.PerspectiveCamera(); + cameraR.layers.enable( 2 ); + + this.render = function( scene, camera, renderTarget, forceClear ) { + + if ( vrDisplay && scope.isPresenting ) { + + var autoUpdate = scene.autoUpdate; + + if ( autoUpdate ) { + + scene.updateMatrixWorld(); + scene.autoUpdate = false; + + } + + var eyeParamsL = vrDisplay.getEyeParameters( 'left' ); + var eyeParamsR = vrDisplay.getEyeParameters( 'right' ); + + eyeTranslationL.fromArray( eyeParamsL.offset ); + eyeTranslationR.fromArray( eyeParamsR.offset ); + + if ( Array.isArray( scene ) ) { + + console.warn( 'THREE.VREffect.render() no longer supports arrays. Use object.layers instead.' ); + scene = scene[ 0 ]; + + } + + // When rendering we don't care what the recommended size is, only what the actual size + // of the backbuffer is. + var size = renderer.getSize(); + var layers = vrDisplay.getLayers(); + var leftBounds; + var rightBounds; + + if ( layers.length ) { + + var layer = layers[ 0 ]; + + leftBounds = layer.leftBounds !== null && layer.leftBounds.length === 4 ? layer.leftBounds : defaultLeftBounds; + rightBounds = layer.rightBounds !== null && layer.rightBounds.length === 4 ? layer.rightBounds : defaultRightBounds; + + } else { + + leftBounds = defaultLeftBounds; + rightBounds = defaultRightBounds; + + } + + renderRectL = { + x: Math.round( size.width * leftBounds[ 0 ] ), + y: Math.round( size.height * leftBounds[ 1 ] ), + width: Math.round( size.width * leftBounds[ 2 ] ), + height: Math.round( size.height * leftBounds[ 3 ] ) + }; + renderRectR = { + x: Math.round( size.width * rightBounds[ 0 ] ), + y: Math.round( size.height * rightBounds[ 1 ] ), + width: Math.round( size.width * rightBounds[ 2 ] ), + height: Math.round( size.height * rightBounds[ 3 ] ) + }; + + if ( renderTarget ) { + + renderer.setRenderTarget( renderTarget ); + renderTarget.scissorTest = true; + + } else { + + renderer.setRenderTarget( null ); + renderer.setScissorTest( true ); + + } + + if ( renderer.autoClear || forceClear ) renderer.clear(); + + if ( camera.parent === null ) camera.updateMatrixWorld(); + + camera.matrixWorld.decompose( cameraL.position, cameraL.quaternion, cameraL.scale ); + camera.matrixWorld.decompose( cameraR.position, cameraR.quaternion, cameraR.scale ); + + var scale = this.scale; + cameraL.translateOnAxis( eyeTranslationL, scale ); + cameraR.translateOnAxis( eyeTranslationR, scale ); + + if ( vrDisplay.getFrameData ) { + + vrDisplay.depthNear = camera.near; + vrDisplay.depthFar = camera.far; + + vrDisplay.getFrameData( frameData ); + + cameraL.projectionMatrix.elements = frameData.leftProjectionMatrix; + cameraR.projectionMatrix.elements = frameData.rightProjectionMatrix; + + } else { + + cameraL.projectionMatrix = fovToProjection( eyeParamsL.fieldOfView, true, camera.near, camera.far ); + cameraR.projectionMatrix = fovToProjection( eyeParamsR.fieldOfView, true, camera.near, camera.far ); + + } + + // render left eye + if ( renderTarget ) { + + renderTarget.viewport.set( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); + renderTarget.scissor.set( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); + + } else { + + renderer.setViewport( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); + renderer.setScissor( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); + + } + renderer.render( scene, cameraL, renderTarget, forceClear ); + + // render right eye + if ( renderTarget ) { + + renderTarget.viewport.set( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); + renderTarget.scissor.set( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); + + } else { + + renderer.setViewport( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); + renderer.setScissor( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); + + } + renderer.render( scene, cameraR, renderTarget, forceClear ); + + if ( renderTarget ) { + + renderTarget.viewport.set( 0, 0, size.width, size.height ); + renderTarget.scissor.set( 0, 0, size.width, size.height ); + renderTarget.scissorTest = false; + renderer.setRenderTarget( null ); + + } else { + + renderer.setViewport( 0, 0, size.width, size.height ); + renderer.setScissorTest( false ); + + } + + if ( autoUpdate ) { + + scene.autoUpdate = true; + + } + + if ( scope.autoSubmitFrame ) { + + scope.submitFrame(); + + } + + return; + + } + + // Regular render mode if not HMD + + renderer.render( scene, camera, renderTarget, forceClear ); + + }; + + this.dispose = function() { + + window.removeEventListener( 'vrdisplaypresentchange', onVRDisplayPresentChange, false ); + + }; + + // + + function fovToNDCScaleOffset( fov ) { + + var pxscale = 2.0 / ( fov.leftTan + fov.rightTan ); + var pxoffset = ( fov.leftTan - fov.rightTan ) * pxscale * 0.5; + var pyscale = 2.0 / ( fov.upTan + fov.downTan ); + var pyoffset = ( fov.upTan - fov.downTan ) * pyscale * 0.5; + return { scale: [ pxscale, pyscale ], offset: [ pxoffset, pyoffset ] }; + + } + + function fovPortToProjection( fov, rightHanded, zNear, zFar ) { + + rightHanded = rightHanded === undefined ? true : rightHanded; + zNear = zNear === undefined ? 0.01 : zNear; + zFar = zFar === undefined ? 10000.0 : zFar; + + var handednessScale = rightHanded ? - 1.0 : 1.0; + + // start with an identity matrix + var mobj = new THREE.Matrix4(); + var m = mobj.elements; + + // and with scale/offset info for normalized device coords + var scaleAndOffset = fovToNDCScaleOffset( fov ); + + // X result, map clip edges to [-w,+w] + m[ 0 * 4 + 0 ] = scaleAndOffset.scale[ 0 ]; + m[ 0 * 4 + 1 ] = 0.0; + m[ 0 * 4 + 2 ] = scaleAndOffset.offset[ 0 ] * handednessScale; + m[ 0 * 4 + 3 ] = 0.0; + + // Y result, map clip edges to [-w,+w] + // Y offset is negated because this proj matrix transforms from world coords with Y=up, + // but the NDC scaling has Y=down (thanks D3D?) + m[ 1 * 4 + 0 ] = 0.0; + m[ 1 * 4 + 1 ] = scaleAndOffset.scale[ 1 ]; + m[ 1 * 4 + 2 ] = - scaleAndOffset.offset[ 1 ] * handednessScale; + m[ 1 * 4 + 3 ] = 0.0; + + // Z result (up to the app) + m[ 2 * 4 + 0 ] = 0.0; + m[ 2 * 4 + 1 ] = 0.0; + m[ 2 * 4 + 2 ] = zFar / ( zNear - zFar ) * - handednessScale; + m[ 2 * 4 + 3 ] = ( zFar * zNear ) / ( zNear - zFar ); + + // W result (= Z in) + m[ 3 * 4 + 0 ] = 0.0; + m[ 3 * 4 + 1 ] = 0.0; + m[ 3 * 4 + 2 ] = handednessScale; + m[ 3 * 4 + 3 ] = 0.0; + + mobj.transpose(); + + return mobj; + + } + + function fovToProjection( fov, rightHanded, zNear, zFar ) { + + var DEG2RAD = Math.PI / 180.0; + + var fovPort = { + upTan: Math.tan( fov.upDegrees * DEG2RAD ), + downTan: Math.tan( fov.downDegrees * DEG2RAD ), + leftTan: Math.tan( fov.leftDegrees * DEG2RAD ), + rightTan: Math.tan( fov.rightDegrees * DEG2RAD ) + }; + + return fovPortToProjection( fovPort, rightHanded, zNear, zFar ); + + } + +}; diff --git a/js/lib/WebVR.js b/js/lib/WebVR.js new file mode 100644 index 0000000..0c37d1b --- /dev/null +++ b/js/lib/WebVR.js @@ -0,0 +1,102 @@ +/** + * @author mrdoob / http://mrdoob.com + * Based on @tojiro's vr-samples-utils.js + */ + +var WEBVR = { + + isLatestAvailable: function () { + + console.warn( 'WEBVR: isLatestAvailable() is being deprecated. Use .isAvailable() instead.' ); + return this.isAvailable(); + + }, + + isAvailable: function () { + + return navigator.getVRDisplays !== undefined; + + }, + + getMessage: function () { + + var message; + + if ( navigator.getVRDisplays ) { + + navigator.getVRDisplays().then( function ( displays ) { + + if ( displays.length === 0 ) message = 'WebVR supported, but no VRDisplays found.'; + + } ); + + } else { + + message = 'Your browser does not support WebVR. See webvr.info for assistance.'; + + } + + if ( message !== undefined ) { + + var container = document.createElement( 'div' ); + container.style.position = 'absolute'; + container.style.left = '0'; + container.style.top = '0'; + container.style.right = '0'; + container.style.zIndex = '999'; + container.align = 'center'; + + var error = document.createElement( 'div' ); + error.style.fontFamily = 'sans-serif'; + error.style.fontSize = '16px'; + error.style.fontStyle = 'normal'; + error.style.lineHeight = '26px'; + error.style.backgroundColor = '#fff'; + error.style.color = '#000'; + error.style.padding = '10px 20px'; + error.style.margin = '50px'; + error.style.display = 'inline-block'; + error.innerHTML = message; + container.appendChild( error ); + + return container; + + } + + }, + + getButton: function ( effect ) { + + var button = document.createElement( 'button' ); + button.style.position = 'absolute'; + button.style.left = 'calc(50% - 50px)'; + button.style.bottom = '20px'; + button.style.width = '100px'; + button.style.border = '0'; + button.style.padding = '8px'; + button.style.cursor = 'pointer'; + button.style.backgroundColor = '#000'; + button.style.color = '#fff'; + button.style.fontFamily = 'sans-serif'; + button.style.fontSize = '13px'; + button.style.fontStyle = 'normal'; + button.style.textAlign = 'center'; + button.style.zIndex = '999'; + button.textContent = 'ENTER VR'; + button.onclick = function() { + + effect.isPresenting ? effect.exitPresent() : effect.requestPresent(); + + }; + + window.addEventListener( 'vrdisplaypresentchange', function ( event ) { + + button.textContent = effect.isPresenting ? 'EXIT VR' : 'ENTER VR'; + + }, false ); + + return button; + + } + +};