Imported "ThreeJS + WebVR" Boilerplate

This commit is contained in:
raccoon
2024-06-26 00:07:18 +05:00
commit 44a9471582
11 changed files with 1468 additions and 0 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
* text=auto eol=lf

61
.gitignore vendored Normal file
View File

@@ -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

54
README.md Normal file
View File

@@ -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

221
index.html Normal file
View File

@@ -0,0 +1,221 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebVR + ThreeJS Application</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<!-- required -->
<script src="https://cdn.jsdelivr.net/npm/three@0.96.0/build/three.min.js"></script>
<!-- needed for loading GLTF files -->
<script src="https://cdn.jsdelivr.net/npm/three@0.96.0/examples/js/loaders/GLTFLoader.js"></script>
<!-- needed for loading Truetype Fonts -->
<script src="https://cdn.jsdelivr.net/npm/three@0.96.0/examples/js/loaders/TTFLoader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.96.0/examples/js/libs/opentype.min.js"></script>
<!-- needed to enter VR -->
<script src="./static/js/webvr.js"></script>
<!-- stats (fps, polygons, fps) -->
<script src="./static/js/vrstats.js" type="module"></script>
<!-- mouse/touch/vr-controller support -->
<script src="./static/js/pointer.js" type="module"></script>
<style type="text/css">
html, body {
margin:0;
padding:0;
overflow: hidden;
}
#overlay {
position: fixed;
font-size: 5vh;
width: 100vw;
height: 100vh;
background-color: rgba(0,0,0,0.5);
text-align: center;
}
#loading-indicator {
display: block;
}
#click-to-play {
/*display: none;*/
color: black;
background-color: white;
border: 1px solid black;
}
/* this button is generated by the VR subsystem, disabled if not available */
#enter-vr {
position: absolute;
bottom: 20px;
left: 50%;
transform: translate(-50%,0);
}
</style>
</head>
<body>
<div id="overlay">
<h1>Application Name</h1>
<div id="loading-indicator">
<label>loading</label>
<progress max="100" value="0" id="progress"></progress>
</div>
<h3 id="click-to-play">click to start</h3>
</div>
<script type="module">
import {POINTER_CLICK, POINTER_ENTER, POINTER_EXIT, Pointer} from './static/js/pointer.js'
import VRStats from "./static/js/vrstats.js"
//JQuery-like selector
const $ = (sel) => document.querySelector(sel)
const on = (elem, type, cb) => elem.addEventListener(type,cb)
// global constants and variables for your app go here
let camera, scene, renderer, pointer, stats;
let cube
//called on setup. Customize this
function initContent(scene,camera,renderer) {
//set the background color of the scene
scene.background = new THREE.Color( 0xcccccc );
//load a cat texture
const texture_loader = new THREE.TextureLoader()
//cat from http://creative-commons-cats.tumblr.com/page/3
const texture = texture_loader.load('./static/img/cat.jpg')
//create a cube
cube = new THREE.Mesh(
new THREE.BoxGeometry(1,1,1),
new THREE.MeshLambertMaterial({color:'white', map:texture})
)
//camera is at z=0, so move the cube back so we can see it
cube.position.z = -5
//move cube up to camera height (~1.5m)
cube.position.y = 1.5
//make it clickable
cube.userData.clickable = true
scene.add(cube)
//a standard light
const light = new THREE.DirectionalLight( 0xffffff, 1.0 );
light.position.set( 1, 1, 1 ).normalize();
scene.add( light );
// enable stats visible inside VR
stats = new VRStats(renderer)
camera.add(stats)
scene.add(camera)
//class which handles mouse and VR controller
pointer = new Pointer(scene,renderer,camera, {
//Pointer searches everything in the scene by default
//override this to match just certain things
intersectionFilter: ((o) => o.userData.clickable),
//make the camera pan when moving the mouse. good for simulating head turning on desktop
cameraFollowMouse:false,
// set to true to move the controller node forward and tilt with the mouse.
// good for testing VR controls on desktop
mouseSimulatesController:false,
})
//change cube to red BG when clicking
on(cube,POINTER_CLICK,()=>{
console.log("clicking on the cube")
cube.material.color.set(0xff0000)
})
//change cube to green BG when hovering over it
on(cube,POINTER_ENTER,()=>{
console.log("entering the cube")
cube.material.color.set(0x00ff00)
})
on(cube,POINTER_EXIT,()=>{
console.log('exiting the cube')
cube.material.color.set(0xffffff)
})
}
//called on every frame. customize this
function render(time) {
//update the pointer and stats, if configured
if(pointer) pointer.tick(time)
if(stats) stats.update(time)
//rotate the cube on every tick
if(cube) cube.rotation.y += 0.002
renderer.render( scene, camera );
}
// you shouldn't need to modify much below here
function initScene() {
//create DIV for the canvas
const container = document.createElement( 'div' );
document.body.appendChild( container );
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 0.1, 50 );
renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.gammaOutput = true
renderer.vr.enabled = true;
container.appendChild( renderer.domElement );
document.body.appendChild( WEBVR.createButton( renderer ) );
initContent(scene,camera,renderer)
window.addEventListener( 'resize', ()=>{
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
}, false );
THREE.DefaultLoadingManager.onStart = (url, loaded, total) => {
console.log(`loading ${url}. loaded ${loaded} of ${total}`)
}
THREE.DefaultLoadingManager.onLoad = () => {
console.log(`loading complete`)
console.log("really setting it up now")
$('#loading-indicator').style.display = 'none'
$('#click-to-play').style.display = 'block'
const overlay = $('#overlay')
$("#click-to-play").addEventListener('click',()=>{
overlay.style.visibility = 'hidden'
if($('#enter-vr')) $('#enter-vr').removeAttribute('disabled')
})
}
THREE.DefaultLoadingManager.onProgress = (url, loaded, total) => {
console.log(`prog ${url}. loaded ${loaded} of ${total}`)
$("#progress").setAttribute('value',100*(loaded/total))
}
THREE.DefaultLoadingManager.onError = (url) => {
console.log(`error loading ${url}`)
}
}
// initPage()
initScene()
renderer.setAnimationLoop(render)
</script>
</body>
</html>

BIN
static/img/cat.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

139
static/js/Raycaster.js Normal file
View File

@@ -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 };

305
static/js/pointer.js Normal file
View File

@@ -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 {
}
}
}

103
static/js/vrmanager.js Normal file
View File

@@ -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}]);
}
}
}

61
static/js/vrstats.js Normal file
View File

@@ -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
}
}

242
static/js/webvr.js Normal file
View File

@@ -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.' );
}
};

281
v2.html Normal file
View File

@@ -0,0 +1,281 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Fathom - simple website analytics - https://github.com/usefathom/fathom -->
<script>
(function(f, a, t, h, o, m){
a[h]=a[h]||function(){
(a[h].q=a[h].q||[]).push(arguments)
};
o=f.createElement('script'),
m=f.getElementsByTagName('script')[0];
o.async=1; o.src=t; o.id='fathom-script';
m.parentNode.insertBefore(o,m)
})(document, window, '//stats.josh.earth/tracker.js', 'fathom');
fathom('set', 'siteId', 'GISNV');
fathom('trackPageview');
</script>
<!-- / Fathom --> <meta charset="UTF-8">
<title>WebVR + ThreeJS Application</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<!-- required for everything -->
<script src="https://cdn.jsdelivr.net/npm/three@0.96.0/build/three.min.js"></script>
<!-- needed for loading GLTF files -->
<script src="https://cdn.jsdelivr.net/npm/three@0.96.0/examples/js/loaders/GLTFLoader.js"></script>
<style type="text/css">
html, body {
margin:0;
padding:0;
overflow: hidden;
}
button {
color: black;
background-color: white;
border: 1px solid black;
font-size: 100%;
padding: 0.25em;
margin: 0.25em;
}
button:disabled {
color: darkgray;
background-color: gray;
}
#overlay {
position: fixed;
font-size: 5vh;
width: 100vw;
height: 100vh;
background-color: rgba(255,255,255,0.5);
text-align: center;
display: flex;
flex-direction: row;
justify-content: center;
align-content: center;
}
#overlay #inner {
border: 1px solid black;
background-color: white;
width: 70vw;
height: 40vh;
display: flex;
flex-direction: column;
align-items: center;
}
#loading-indicator {
display: block;
}
#start-button {
display: none;
}
</style>
</head>
<body>
<div id="overlay">
<div id="inner">
<h1>Application Name</h1>
<div id="loading-indicator">
<label>loading</label>
<progress max="100" value="0" id="progress"></progress>
</div>
<button id="enter-button" disabled>VR not supported, play anyway</button>
</div>
</div>
<script type="module">
// for pointer (mouse, controller, touch) support
import {POINTER_CLICK, POINTER_ENTER, POINTER_EXIT, Pointer} from './static/js/pointer.js'
// calculate FPS and other stats
import VRStats from "./static/js/vrstats.js"
// enter and exit VR
import VRManager, {VR_DETECTED} from "./static/js/vrmanager.js"
//JQuery-like selector
const $ = (sel) => document.querySelector(sel)
const on = (elem, type, cb) => elem.addEventListener(type,cb)
// global constants and variables for your app go here
let camera, scene, renderer, pointer, stats, vrmanager;
let cube
const WAIT_FOR_LOAD = false
//called on setup. Customize this
function initContent(scene,camera,renderer) {
//set the background color of the scene
scene.background = new THREE.Color( 0xcccccc );
//load a cat texture
const texture_loader = new THREE.TextureLoader()
//cat from http://creative-commons-cats.tumblr.com/page/3
const texture = texture_loader.load('./static/img/cat.jpg')
//create a cube
cube = new THREE.Mesh(
new THREE.BoxGeometry(1,1,1),
new THREE.MeshLambertMaterial({color:'white', map:texture})
)
//camera is at z=0, so move the cube back so we can see it
cube.position.z = -5
//move cube up to camera height (~1.5m)
cube.position.y = 1.5
//make it clickable
cube.userData.clickable = true
scene.add(cube)
//a standard light
const light = new THREE.DirectionalLight( 0xffffff, 1.0 );
light.position.set( 1, 1, 1 ).normalize();
scene.add( light );
// enable stats visible inside VR
stats = new VRStats(renderer)
camera.add(stats)
scene.add(camera)
//class which handles mouse and VR controller
pointer = new Pointer(scene,renderer,camera, {
//Pointer searches everything in the scene by default
//override this to match just certain things
intersectionFilter: ((o) => o.userData.clickable),
//make the camera pan when moving the mouse. good for simulating head turning on desktop
cameraFollowMouse:false,
// set to true to move the controller node forward and tilt with the mouse.
// good for testing VR controls on desktop
mouseSimulatesController:false,
//turn this off if you provide your own pointer model
enableLaser: true,
})
const STICK_HEIGHT = 1.0
const stick = new THREE.Mesh(
new THREE.CylinderBufferGeometry(0.1,0.1,STICK_HEIGHT),
new THREE.MeshLambertMaterial({color:'aqua'})
)
const toRad = (degrees) => degrees*Math.PI/180
stick.position.z = -STICK_HEIGHT/2;
stick.rotation.x = toRad(-90)
pointer.controller1.add(stick)
//change cube to red BG when clicking
on(cube,POINTER_CLICK,()=>{
console.log("clicking on the cube")
cube.material.color.set(0xff0000)
})
//change cube to green BG when hovering over it
on(cube,POINTER_ENTER,()=>{
// console.log("entering the cube")
cube.material.color.set(0x00ff00)
})
on(cube,POINTER_EXIT,()=>{
// console.log('exiting the cube')
cube.material.color.set(0xffffff)
})
on($("#enter-button"),'click',()=>{
$("#overlay").style.display = 'none'
//we can start playing sound now
})
// this will fire if VR is supported on the device
// and a VR headset is detected
// if it never fires assume VR is not supported at all.
on(vrmanager,VR_DETECTED,()=>{
console.log("VR detected")
$("#enter-button").removeAttribute('disabled',false)
$("#enter-button").innerText = "enter vr"
on($("#enter-button"),'click',()=> vrmanager.enterVR())
})
}
//called on every frame. customize this
function render(time) {
//update the pointer and stats, if configured
if(pointer) pointer.tick(time)
if(stats) stats.update(time)
//rotate the cube on every tick
if(cube) cube.rotation.y += 0.002
renderer.render( scene, camera );
}
// you shouldn't need to modify much below here
function initScene() {
//create DIV for the canvas
const container = document.createElement( 'div' );
document.body.appendChild( container );
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 0.1, 50 );
renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.gammaOutput = true
renderer.vr.enabled = true;
container.appendChild( renderer.domElement );
vrmanager = new VRManager(renderer)
// document.body.appendChild( WEBVR.createButton( renderer ) );
initContent(scene,camera,renderer)
window.addEventListener( 'resize', ()=>{
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
}, false );
THREE.DefaultLoadingManager.onStart = (url, loaded, total) => {
console.log(`loading ${url}. loaded ${loaded} of ${total}`)
}
THREE.DefaultLoadingManager.onLoad = () => {
console.log(`loading complete`)
$("#loading-indicator").style.display = 'none'
$("#enter-button").style.display = 'block'
$("#enter-button").removeAttribute('disabled')
}
THREE.DefaultLoadingManager.onProgress = (url, loaded, total) => {
console.log(`prog ${url}. loaded ${loaded} of ${total}`)
$("#progress").setAttribute('value',100*(loaded/total))
}
THREE.DefaultLoadingManager.onError = (url) => {
console.log(`error loading ${url}`)
}
if(!WAIT_FOR_LOAD) {
$("#loading-indicator").style.display = 'none'
$("#enter-button").style.display = 'block'
$("#enter-button").removeAttribute('disabled')
}
}
// initPage()
initScene()
renderer.setAnimationLoop(render)
</script>
</body>
</html>