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

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