commit 44a9471582717815162c10f0434b3b321d5533fa Author: raccoon Date: Wed Jun 26 00:07:18 2024 +0500 Imported "ThreeJS + WebVR" Boilerplate diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad46b30 --- /dev/null +++ b/.gitignore @@ -0,0 +1,61 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# next.js build output +.next diff --git a/README.md b/README.md new file mode 100644 index 0000000..7328e36 --- /dev/null +++ b/README.md @@ -0,0 +1,54 @@ +# ThreeJS + WebVR Boilerplate + + +## How to use this boilerplate + +* Copy or fork this repo +* install threejs using `npm install` +* customize `initContent()` with whatever you want + + + +# Notes + +The `Pointer` class unifies mouse, touch and vr controller events into special POINTER events. +It does not yet handle cardboard / gaze input, but that is coming. To remove it just don't initialize it. To capture +the events add listeners for them: + +* `POINTER_ENTER`: fires when a ray cast from the pointer enters an object in the scene +* `POINTER_PRESS`: fires when the pointer is pressed down and a ray cast from the pointer intersects an object in the scene +* `POINTER_RELEASE`: fires when the pointer is pressed down and a ray cast from the pointer intersects an object in the scene +* `POINTER_EXIT`: fires when a ray cast from the pointer exits an object in the scene +* `POINTER_CLICK`: fires when the mouse/finger/vr controller is released and a ray cast +enters an object in the scene. +* `POINTER_MOVE`: fires whenever the mouse, controller, or finger moves. + +To listen for clicking on a cube do: + +``` +cube.addEventListener(POINTER_CLICK, (e)=>{ + console.log("clicked on cube at ", e.point) +}) +``` + +Note that all of these events fire *on an object* that intersects the ray from the pointer. You will not get events from +the pointer itself. This means you will not receive events if the scene is empty. To get events as the user moves +the pointer around, regardless of what the pointer is pointing at, create an invisible sphere around the user/camera and listen for +events on that. + +The `VRStats` class gives you stats *within* VR. To remove it just don't initialize it. + +The progress bar is tied to the default loader. If you aren't loading anything, meaning no textures or +fonts or sounds, then the progress events will never fire and it will never dismiss the overlay. In this +case simply delete the overlay. + + +# Todos + +* *fixed* clicking does not work inside of VR +* a way to customize the ray object easily +* handle the nothing to load case +* support touch events +* support gaze cursor for zero-button cases + + diff --git a/index.html b/index.html new file mode 100644 index 0000000..62ad46b --- /dev/null +++ b/index.html @@ -0,0 +1,221 @@ + + + + + + WebVR + ThreeJS Application + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Application Name

+
+ + +
+

click to start

+
+ + + + diff --git a/static/img/cat.jpg b/static/img/cat.jpg new file mode 100644 index 0000000..2d22f9b Binary files /dev/null and b/static/img/cat.jpg differ diff --git a/static/js/Raycaster.js b/static/js/Raycaster.js new file mode 100644 index 0000000..fc7546a --- /dev/null +++ b/static/js/Raycaster.js @@ -0,0 +1,139 @@ +const Ray = THREE.Ray +/** + * @author mrdoob / http://mrdoob.com/ + * @author bhouston / http://clara.io/ + * @author stephomi / http://stephaneginier.com/ + */ + +function Raycaster( origin, direction, near, far ) { + + this.ray = new Ray( origin, direction ); + // direction is assumed to be normalized (for accurate distance calculations) + + this.near = near || 0; + this.far = far || Infinity; + + this.params = { + Mesh: {}, + Line: {}, + LOD: {}, + Points: { threshold: 1 }, + Sprite: {} + }; + + Object.defineProperties( this.params, { + PointCloud: { + get: function () { + + console.warn( 'THREE.Raycaster: params.PointCloud has been renamed to params.Points.' ); + return this.Points; + + } + } + } ); + +} + +function ascSort( a, b ) { + + return a.distance - b.distance; + +} + +let count = 0 + +function intersectObject( object, raycaster, intersects, recursive ) { + + if ( object.visible === false ) return; + if(raycaster.recurseFilter && !raycaster.recurseFilter(object)) return; + + count++ + object.raycast( raycaster, intersects ); + + if ( recursive === true ) { + + var children = object.children; + + for ( var i = 0, l = children.length; i < l; i ++ ) { + + intersectObject( children[ i ], raycaster, intersects, true ); + + } + + } + +} + +Object.assign( Raycaster.prototype, { + + linePrecision: 1, + + set: function ( origin, direction ) { + + // direction is assumed to be normalized (for accurate distance calculations) + + this.ray.set( origin, direction ); + + }, + + setFromCamera: function ( coords, camera ) { + + if ( ( camera && camera.isPerspectiveCamera ) ) { + + this.ray.origin.setFromMatrixPosition( camera.matrixWorld ); + this.ray.direction.set( coords.x, coords.y, 0.5 ).unproject( camera ).sub( this.ray.origin ).normalize(); + + } else if ( ( camera && camera.isOrthographicCamera ) ) { + + this.ray.origin.set( coords.x, coords.y, ( camera.near + camera.far ) / ( camera.near - camera.far ) ).unproject( camera ); // set origin in plane of camera + this.ray.direction.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld ); + + } else { + + console.error( 'THREE.Raycaster: Unsupported camera type.' ); + + } + + }, + + intersectObject: function ( object, recursive, optionalTarget ) { + + var intersects = optionalTarget || []; + + intersectObject( object, this, intersects, recursive); + + intersects.sort( ascSort ); + + return intersects; + + }, + + intersectObjects: function ( objects, recursive, optionalTarget ) { + count = 0 + var intersects = optionalTarget || []; + + if ( Array.isArray( objects ) === false ) { + + console.warn( 'THREE.Raycaster.intersectObjects: objects is not an Array.' ); + return intersects; + + } + + for ( var i = 0, l = objects.length; i < l; i ++ ) { + + intersectObject( objects[ i ], this, intersects, recursive); + + } + + // console.log("intersected objects",count) + + intersects.sort( ascSort ); + + return intersects; + + } + +} ); + + +export { Raycaster }; diff --git a/static/js/pointer.js b/static/js/pointer.js new file mode 100644 index 0000000..581d59a --- /dev/null +++ b/static/js/pointer.js @@ -0,0 +1,305 @@ +import {Raycaster} from "./Raycaster.js" +export const POINTER_ENTER = "enter" +export const POINTER_EXIT = "exit" +export const POINTER_CLICK = "click" +export const POINTER_MOVE = "move" +export const POINTER_PRESS = "press" +export const POINTER_RELEASE = "release" + +// import * as THREE from "./node_modules/three/build/three.module.js" + +const toRad = (degrees) => degrees*Math.PI/180 + +export class Pointer { + constructor(scene, renderer, camera, opts) { + this.listeners = {} + this.opts = opts || {} + this.opts.enableLaser = (opts.enableLaser !== undefined) ? opts.enableLaser : true + this.opts.laserLength = (opts.laserLength !== undefined) ? opts.laserLength : 3 + this.opts.enableMoveEvents = (opts.enableMoveEvents !== undefined) ? opts.enableMoveEvents : true + this.scene = scene + this.renderer = renderer + this.canvas = renderer.domElement + this.camera = camera + + this.raycaster = new Raycaster() + this.waitcb = null + this.hoverTarget = null + + this.intersectionFilter = this.opts.intersectionFilter || (() => true) + this.raycaster.recurseFilter = this.opts.recurseFilter || (()=> true) + + + // setup the mouse + this.canvas.addEventListener('mousemove', this.mouseMove.bind(this)) + this.canvas.addEventListener('click', this.mouseClick.bind(this)) + this.canvas.addEventListener('mousedown',this.mouseDown.bind(this)) + this.canvas.addEventListener('mouseup',this.mouseUp.bind(this)) + + //touch events + this.canvas.addEventListener('touchstart',this.touchStart.bind(this)) + this.canvas.addEventListener('touchmove',this.touchMove.bind(this)) + this.canvas.addEventListener('touchend',this.touchEnd.bind(this)) + + // setup the VR controllers + this.controller1 = this.renderer.vr.getController(0); + this.controller1.addEventListener('selectstart', this.controllerSelectStart.bind(this)); + this.controller1.addEventListener('selectend', this.controllerSelectEnd.bind(this)); + + + this.controller2 = this.renderer.vr.getController(1); + this.controller2.addEventListener('selectstart', this.controllerSelectStart.bind(this)); + this.controller2.addEventListener('selectend', this.controllerSelectEnd.bind(this)); + + this.setMouseSimulatesController(opts.mouseSimulatesController) + + this.scene.add(this.controller1); + this.scene.add(this.controller2); + + + + if(this.opts.enableLaser) { + //create visible lines for the two controllers + const geometry = new THREE.BufferGeometry() + geometry.addAttribute('position', new THREE.Float32BufferAttribute([0, 0, 0, 0, 0, -this.opts.laserLength], 3)); + geometry.addAttribute('color', new THREE.Float32BufferAttribute([1.0, 0.5, 0.5, 0, 0, 0], 3)); + + const material = new THREE.LineBasicMaterial({ + vertexColors: false, + color: 0x880000, + linewidth: 5, + blending: THREE.NormalBlending + }) + + this.controller1.add(new THREE.Line(geometry, material)); + this.controller2.add(new THREE.Line(geometry, material)); + } + + } + + //override this to do something w/ the controllers on every tick + tick(time) { + this.controllerMove(this.controller1) + this.controllerMove(this.controller2) + } + + + fire(obj, type, payload) { + obj.dispatchEvent(payload) + } + fireSelf(type,payload) { + if(!this.listeners[type]) return + this.listeners[type].forEach(cb => cb(payload)) + } + + //make the camera follow the mouse in desktop mode. Helps w/ debugging. + cameraFollowMouse(e) { + const bounds = this.canvas.getBoundingClientRect() + const ry = ((e.clientX - bounds.left) / bounds.width) * 2 - 1 + const rx = 1 - ((e.clientY - bounds.top) / bounds.height) * 2 + this.camera.rotation.y = -ry*2 + this.camera.rotation.x = +rx + } + + mouseMove(e) { + const mouse = new THREE.Vector2() + const bounds = this.canvas.getBoundingClientRect() + mouse.x = ((e.clientX - bounds.left) / bounds.width) * 2 - 1 + mouse.y = -((e.clientY - bounds.top) / bounds.height) * 2 + 1 + this.raycaster.setFromCamera(mouse, this.camera) + if(this.opts.mouseSimulatesController) { + //create target from the mouse controls + const target = new THREE.Vector3() + target.x = mouse.x + target.y = mouse.y + target.z = -3 + //convert to camera space + target.add(this.camera.position) + this.spot.position.copy(target) + this.controller1.lookAt(target) + //have to flip over because the UP is down on controllers + const flip = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0,1,0),toRad(180)) + this.controller1.quaternion.multiply(flip) + } + this._processMove() + + if(this.opts.cameraFollowMouse) this.cameraFollowMouse(e) + } + + touchStart(e) { + e.preventDefault() + if(e.changedTouches.length <= 0) return + const tch = e.changedTouches[0] + const mouse = new THREE.Vector2() + const bounds = this.canvas.getBoundingClientRect() + mouse.x = ((tch.clientX - bounds.left) / bounds.width) * 2 - 1 + mouse.y = -((tch.clientY - bounds.top) / bounds.height) * 2 + 1 + this.raycaster.setFromCamera(mouse, this.camera) + const intersects = this.raycaster.intersectObjects(this.scene.children, true) + .filter(it => this.intersectionFilter(it.object)) + intersects.forEach((it) => { + this.fire(it.object, POINTER_PRESS, {type: POINTER_PRESS}) + }) + } + touchMove(e) { + e.preventDefault() + if(e.changedTouches.length <= 0) return + const tch = e.changedTouches[0] + const mouse = new THREE.Vector2() + const bounds = this.canvas.getBoundingClientRect() + mouse.x = ((tch.clientX - bounds.left) / bounds.width) * 2 - 1 + mouse.y = -((tch.clientY - bounds.top) / bounds.height) * 2 + 1 + this.raycaster.setFromCamera(mouse, this.camera) + this._processMove() + } + touchEnd(e) { + e.preventDefault() + if(e.changedTouches.length <= 0) return + const tch = e.changedTouches[0] + const mouse = new THREE.Vector2() + const bounds = this.canvas.getBoundingClientRect() + mouse.x = ((tch.clientX - bounds.left) / bounds.width) * 2 - 1 + mouse.y = -((tch.clientY - bounds.top) / bounds.height) * 2 + 1 + this.raycaster.setFromCamera(mouse, this.camera) + const intersects = this.raycaster.intersectObjects(this.scene.children, true) + .filter(it => this.intersectionFilter(it.object)) + intersects.forEach((it) => { + this.fire(it.object, POINTER_RELEASE, {type: POINTER_RELEASE, point: it.point}) + }) + this._processClick() + } + + controllerMove(controller) { + if(!controller.visible) return + const c = controller + const dir = new THREE.Vector3(0, 0, -1) + dir.applyQuaternion(c.quaternion) + this.raycaster.set(c.position, dir) + this._processMove() + } + + _processMove() { + if(!this.opts.enableMoveEvents)return + const intersects = this.raycaster.intersectObjects(this.scene.children, true) + .filter(it => this.intersectionFilter(it.object)) + + if(intersects.length === 0 && this.hoverTarget) { + this.fire(this.hoverTarget, POINTER_EXIT, {type: POINTER_EXIT}) + this.hoverTarget = null + } + if(intersects.length >= 1) { + const it = intersects[0] + const obj = it.object + if (!obj) return + this.fire(obj, POINTER_MOVE, {type: POINTER_MOVE, point: it.point, intersection:it}) + if (obj === this.hoverTarget) { + //still inside + } else { + if (this.hoverTarget) + this.fire(this.hoverTarget, POINTER_EXIT, {type: POINTER_EXIT}) + this.hoverTarget = obj + this.fire(this.hoverTarget, POINTER_ENTER, {type: POINTER_ENTER}) + } + } + } + + _processClick() { + if (this.waitcb) { + this.waitcb() + this.waitcb = null + return + } + + const intersects = this.raycaster.intersectObjects(this.scene.children, true) + .filter(it => this.intersectionFilter(it.object)) + if(intersects.length > 0) { + const it = intersects[0] + this.fire(it.object, POINTER_CLICK, {type: POINTER_CLICK, point: it.point, intersection:it}) + } + + this.fireSelf(POINTER_CLICK, {}) + } + mouseClick(e) { + const mouse = new THREE.Vector2() + const bounds = this.canvas.getBoundingClientRect() + mouse.x = ((e.clientX - bounds.left) / bounds.width) * 2 - 1 + mouse.y = -((e.clientY - bounds.top) / bounds.height) * 2 + 1 + this.raycaster.setFromCamera(mouse, this.camera) + this._processClick() + } + mouseDown(e) { + const mouse = new THREE.Vector2() + const bounds = this.canvas.getBoundingClientRect() + mouse.x = ((e.clientX - bounds.left) / bounds.width) * 2 - 1 + mouse.y = -((e.clientY - bounds.top) / bounds.height) * 2 + 1 + this.raycaster.setFromCamera(mouse, this.camera) + const intersects = this.raycaster.intersectObjects(this.scene.children, true) + .filter(it => this.intersectionFilter(it.object)) + intersects.forEach((it) => { + this.fire(it.object, POINTER_PRESS, {type: POINTER_PRESS, point: it.point, intersection:it}) + }) + } + mouseUp(e) { + const mouse = new THREE.Vector2() + const bounds = this.canvas.getBoundingClientRect() + mouse.x = ((e.clientX - bounds.left) / bounds.width) * 2 - 1 + mouse.y = -((e.clientY - bounds.top) / bounds.height) * 2 + 1 + this.raycaster.setFromCamera(mouse, this.camera) + const intersects = this.raycaster.intersectObjects(this.scene.children, true) + .filter(it => this.intersectionFilter(it.object)) + intersects.forEach((it) => { + this.fire(it.object, POINTER_RELEASE, {type: POINTER_RELEASE, point: it.point, intersection:it}) + }) + } + + controllerSelectStart(e) { + e.target.userData.isSelecting = true; + const intersects = this.raycaster.intersectObjects(this.scene.children, true) + .filter(it => this.intersectionFilter(it.object)) + intersects.forEach((it) => { + this.fire(it.object, POINTER_PRESS, {type: POINTER_PRESS, point: it.point, intersection:it}) + }) + } + + controllerSelectEnd(e) { + e.target.userData.isSelecting = false; + const c = e.target + const dir = new THREE.Vector3(0, 0, -1) + dir.applyQuaternion(c.quaternion) + this.raycaster.set(c.position, dir) + const intersects = this.raycaster.intersectObjects(this.scene.children, true) + .filter(it => this.intersectionFilter(it.object)) + intersects.forEach((it) => { + this.fire(it.object, POINTER_RELEASE, {type: POINTER_RELEASE, point: it.point}) + }) + this._processClick() + } + + waitSceneClick(cb) { + this.waitcb = cb + } + + + on(type,cb) { + if(!this.listeners[type]) this.listeners[type] = [] + this.listeners[type].push(cb) + } + off(type,cb) { + this.listeners[type] = this.listeners[type].filter(c => c !== cb) + } + setMouseSimulatesController(val) { + this.opts.mouseSimulatesController = val + if(this.opts.mouseSimulatesController) { + this.controller1 = new THREE.Group() + this.controller1.position.set(0,1,-2) + this.controller1.quaternion.setFromUnitVectors(THREE.Object3D.DefaultUp, new THREE.Vector3(0,0,1)) + this.spot = new THREE.Mesh( + new THREE.SphereBufferGeometry(0.1), + new THREE.MeshLambertMaterial({color: 'red'}) + ) + this.scene.add(this.spot) + } else { + } + + } +} diff --git a/static/js/vrmanager.js b/static/js/vrmanager.js new file mode 100644 index 0000000..7afda01 --- /dev/null +++ b/static/js/vrmanager.js @@ -0,0 +1,103 @@ +function printError(err) { + console.log(err) +} + +export const VR_DETECTED = "detected" +export const VR_CONNECTED = "connected" +export const VR_DISCONNECTED = "disconnected" +export const VR_PRESENTCHANGE = "presentchange" +export const VR_ACTIVATED = "activated" + + + +export default class VRManager { + constructor(renderer) { + this.device = null + this.renderer = renderer + if(!this.renderer) throw new Error("VR Manager requires a valid ThreeJS renderer instance") + this.listeners = {} + + if ('xr' in navigator) { + console.log("has webxr") + navigator.xr.requestDevice().then((device) => { + device.supportsSession({immersive: true, exclusive: true /* DEPRECATED */}) + .then(() => { + this.device = device + this.fire(VR_DETECTED,{}) + }) + .catch(printError); + + }).catch(printError); + } else if ('getVRDisplays' in navigator) { + console.log("has webvr") + + window.addEventListener( 'vrdisplayconnect', ( event ) => { + this.device = event.display + this.fire(VR_CONNECTED) + }, false ); + + window.addEventListener( 'vrdisplaydisconnect', ( event ) => { + this.fire(VR_DISCONNECTED) + }, false ); + + window.addEventListener( 'vrdisplaypresentchange', ( event ) => { + this.fire(VR_PRESENTCHANGE) + }, false ); + + window.addEventListener( 'vrdisplayactivate', ( event ) => { + this.device = event.display + this.device.requestPresent([{source:this.renderer.domElement}]) + this.fire(VR_ACTIVATED) + }, false ); + + navigator.getVRDisplays() + .then( ( displays ) => { + console.log("vr scanned") + if ( displays.length > 0 ) { + + // showEnterVR( displays[ 0 ] ); + console.log("found vr",displays[0]) + this.device = displays[0] + this.fire(VR_DETECTED,{}) + + } else { + console.log("no vr at all") + // showVRNotFound(); + } + + } ).catch(printError); + + } else { + // no vr + console.log("no vr at all") + } + } + + addEventListener(type, cb) { + if(!this.listeners[type]) this.listeners[type] = [] + this.listeners[type].push(cb) + } + fire(type,evt) { + if(!evt) evt = {} + evt.type = type + if(!this.listeners[type]) this.listeners[type] = [] + this.listeners[type].forEach(cb => cb(evt)) + } + + enterVR() { + if(!this.device) { + console.warn("tried to connect VR on an invalid device") + return + } + console.log("entering VR") + const prom = this.renderer.vr.setDevice( this.device ); + console.log('promise is',prom) + + if(this.device.isPresenting) { + this.device.exitPresent() + } else { + this.device.requestPresent([{source: this.renderer.domElement}]); + } + } + +} diff --git a/static/js/vrstats.js b/static/js/vrstats.js new file mode 100644 index 0000000..1a99670 --- /dev/null +++ b/static/js/vrstats.js @@ -0,0 +1,61 @@ +export default class VRStats extends THREE.Group { + constructor(renderer) { + super(); + this.renderer = renderer + const can = document.createElement('canvas') + can.width = 256 + can.height = 128 + this.canvas = can + const c = can.getContext('2d') + c.fillStyle = '#00ffff' + c.fillRect(0,0,can.width,can.height) + const ctex = new THREE.CanvasTexture(can) + const mesh = new THREE.Mesh( + new THREE.PlaneGeometry(1,0.5), + // new THREE.BoxGeometry(1,0.5,0.1), + new THREE.MeshBasicMaterial({map:ctex}) + ) + mesh.position.z = -3 + mesh.position.y = 1.5 + mesh.material.depthTest = false + mesh.material.depthWrite = false + mesh.renderOrder = 1000 + this.add(mesh) + this.cmesh = mesh + + this.last = 0 + this.lastFrame = 0 + this.customProps = {} + } + + update(time) { + if(time - this.last > 300) { + // console.log("updating",this.rendereer.info) + // console.log(`stats calls:`,this.renderer.info) + + const fps = ((this.renderer.info.render.frame - this.lastFrame)*1000)/(time-this.last) + // console.log(fps) + + const c = this.canvas.getContext('2d') + c.fillStyle = 'white' + c.fillRect(0, 0, this.canvas.width, this.canvas.height) + c.fillStyle = 'black' + c.font = '16pt sans-serif' + c.fillText(`calls: ${this.renderer.info.render.calls}`, 3, 20) + c.fillText(`tris : ${this.renderer.info.render.triangles}`, 3, 40) + c.fillText(`fps : ${fps.toFixed(2)}`,3,60) + Object.keys(this.customProps).forEach((key,i) => { + const val = this.customProps[key] + c.fillText(`${key} : ${val}`,3,80+i*20) + }) + this.cmesh.material.map.needsUpdate = true + this.last = time + this.lastFrame = this.renderer.info.render.frame + } + } + + setProperty(name, value) { + this.customProps[name] = value + } + +} diff --git a/static/js/webvr.js b/static/js/webvr.js new file mode 100644 index 0000000..ca3afe6 --- /dev/null +++ b/static/js/webvr.js @@ -0,0 +1,242 @@ +/** + * @author mrdoob / http://mrdoob.com + * @author Mugen87 / https://github.com/Mugen87 + * + * Based on @tojiro's vr-samples-utils.js + */ + +var WEBVR = { + + createButton: function ( renderer, options ) { + + // if ( options && options.frameOfReferenceType ) { + // + // renderer.vr.setFrameOfReferenceType( options.frameOfReferenceType ); + // + // } + + function showEnterVR( device ) { + + button.style.display = ''; + + button.style.cursor = 'pointer'; + // button.style.left = 'calc(50% - 50px)'; + // button.style.width = '100px'; + + button.textContent = 'ENTER VR'; + + button.onmouseenter = function () { button.style.opacity = '1.0'; }; + button.onmouseleave = function () { button.style.opacity = '0.5'; }; + + button.onclick = function () { + + device.isPresenting ? device.exitPresent() : device.requestPresent( [ { source: renderer.domElement } ] ); + + }; + + renderer.vr.setDevice( device ); + + } + + function showEnterXR( device ) { + + var currentSession = null; + + function onSessionStarted( session ) { + + session.addEventListener( 'end', onSessionEnded ); + + renderer.vr.setSession( session ); + button.textContent = 'EXIT VR'; + + currentSession = session; + + } + + function onSessionEnded( event ) { + + currentSession.removeEventListener( 'end', onSessionEnded ); + + renderer.vr.setSession( null ); + button.textContent = 'ENTER VR'; + + currentSession = null; + + } + + // + + button.style.display = ''; + + button.style.cursor = 'pointer'; + button.style.left = 'calc(50% - 50px)'; + button.style.width = '100px'; + + button.textContent = 'ENTER VR'; + + button.onmouseenter = function () { button.style.opacity = '1.0'; }; + button.onmouseleave = function () { button.style.opacity = '0.5'; }; + + button.onclick = function () { + + if ( currentSession === null ) { + + device.requestSession( { immersive: true, exclusive: true /* DEPRECATED */ } ).then( onSessionStarted ); + + } else { + + currentSession.end(); + + } + + }; + + renderer.vr.setDevice( device ); + + } + + function showVRNotFound() { + + button.style.display = ''; + + button.style.cursor = 'auto'; + // button.style.left = 'calc(50% - 75px)'; + // button.style.width = '150px'; + + button.textContent = 'VR NOT FOUND'; + + button.onmouseenter = null; + button.onmouseleave = null; + + button.onclick = null; + + renderer.vr.setDevice( null ); + + } + + function stylizeElement( element ) { + // element.style.position = 'absolute'; + // element.style.bottom = '20px'; + /* + + element.style.padding = '12px 6px'; + element.style.border = '1px solid #fff'; + element.style.borderRadius = '4px'; + element.style.background = 'rgba(0,0,0,0.1)'; + element.style.color = '#fff'; + element.style.font = 'normal 13px sans-serif'; + element.style.textAlign = 'center'; + element.style.opacity = '0.5'; + element.style.outline = 'none'; + */ + // element.style.zIndex = '999'; + + } + + if ( 'xr' in navigator ) { + + var button = document.createElement( 'button' ); + button.style.display = 'none'; + + stylizeElement( button ); + + navigator.xr.requestDevice().then( function ( device ) { + + device.supportsSession( { immersive: true, exclusive: true /* DEPRECATED */ } ) + .then( function () { showEnterXR( device ); } ) + .catch( showVRNotFound ); + + } ).catch( showVRNotFound ); + + return button; + + } else if ( 'getVRDisplays' in navigator ) { + + var button = document.createElement( 'button' ); + button.setAttribute("id","enter-vr") + button.setAttribute("disabled",true) + button.style.display = 'none'; + + stylizeElement( button ); + + window.addEventListener( 'vrdisplayconnect', function ( event ) { + + showEnterVR( event.display ); + + }, false ); + + window.addEventListener( 'vrdisplaydisconnect', function ( event ) { + + showVRNotFound(); + + }, false ); + + window.addEventListener( 'vrdisplaypresentchange', function ( event ) { + + button.textContent = event.display.isPresenting ? 'EXIT VR' : 'ENTER VR'; + + }, false ); + + window.addEventListener( 'vrdisplayactivate', function ( event ) { + + event.display.requestPresent( [ { source: renderer.domElement } ] ); + + }, false ); + + navigator.getVRDisplays() + .then( function ( displays ) { + + if ( displays.length > 0 ) { + + showEnterVR( displays[ 0 ] ); + + } else { + + showVRNotFound(); + + } + + } ).catch( showVRNotFound ); + + return button; + + } else { + + var message = document.createElement( 'a' ); + message.href = 'https://webvr.info'; + message.innerHTML = 'WEBVR NOT SUPPORTED'; + + message.style.left = 'calc(50% - 90px)'; + message.style.width = '180px'; + message.style.textDecoration = 'none'; + + stylizeElement( message ); + + return message; + + } + + }, + + // DEPRECATED + + checkAvailability: function () { + console.warn( 'WEBVR.checkAvailability has been deprecated.' ); + return new Promise( function () {} ); + }, + + getMessageContainer: function () { + console.warn( 'WEBVR.getMessageContainer has been deprecated.' ); + return document.createElement( 'div' ); + }, + + getButton: function () { + console.warn( 'WEBVR.getButton has been deprecated.' ); + return document.createElement( 'div' ); + }, + + getVRDisplay: function () { + console.warn( 'WEBVR.getVRDisplay has been deprecated.' ); + } + +}; diff --git a/v2.html b/v2.html new file mode 100644 index 0000000..272a8b7 --- /dev/null +++ b/v2.html @@ -0,0 +1,281 @@ + + + + + + + WebVR + ThreeJS Application + + + + + + + + + + + + + +
+
+

Application Name

+
+ + +
+ +
+
+ + + + +