添加关照、全局等高线、修改图层问题
This commit is contained in:
232
dist/electron/static/sdk/three/jsm/webxr/ARButton.js
vendored
Normal file
232
dist/electron/static/sdk/three/jsm/webxr/ARButton.js
vendored
Normal file
@ -0,0 +1,232 @@
|
||||
class ARButton {
|
||||
|
||||
static createButton( renderer, sessionInit = {} ) {
|
||||
|
||||
const button = document.createElement( 'button' );
|
||||
|
||||
function showStartAR( /*device*/ ) {
|
||||
|
||||
if ( sessionInit.domOverlay === undefined ) {
|
||||
|
||||
const overlay = document.createElement( 'div' );
|
||||
overlay.style.display = 'none';
|
||||
document.body.appendChild( overlay );
|
||||
|
||||
const svg = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' );
|
||||
svg.setAttribute( 'width', 38 );
|
||||
svg.setAttribute( 'height', 38 );
|
||||
svg.style.position = 'absolute';
|
||||
svg.style.right = '20px';
|
||||
svg.style.top = '20px';
|
||||
svg.addEventListener( 'click', function () {
|
||||
|
||||
currentSession.end();
|
||||
|
||||
} );
|
||||
overlay.appendChild( svg );
|
||||
|
||||
const path = document.createElementNS( 'http://www.w3.org/2000/svg', 'path' );
|
||||
path.setAttribute( 'd', 'M 12,12 L 28,28 M 28,12 12,28' );
|
||||
path.setAttribute( 'stroke', '#fff' );
|
||||
path.setAttribute( 'stroke-width', 2 );
|
||||
svg.appendChild( path );
|
||||
|
||||
if ( sessionInit.optionalFeatures === undefined ) {
|
||||
|
||||
sessionInit.optionalFeatures = [];
|
||||
|
||||
}
|
||||
|
||||
sessionInit.optionalFeatures.push( 'dom-overlay' );
|
||||
sessionInit.domOverlay = { root: overlay };
|
||||
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
let currentSession = null;
|
||||
|
||||
async function onSessionStarted( session ) {
|
||||
|
||||
session.addEventListener( 'end', onSessionEnded );
|
||||
|
||||
renderer.xr.setReferenceSpaceType( 'local' );
|
||||
|
||||
await renderer.xr.setSession( session );
|
||||
|
||||
button.textContent = 'STOP AR';
|
||||
sessionInit.domOverlay.root.style.display = '';
|
||||
|
||||
currentSession = session;
|
||||
|
||||
}
|
||||
|
||||
function onSessionEnded( /*event*/ ) {
|
||||
|
||||
currentSession.removeEventListener( 'end', onSessionEnded );
|
||||
|
||||
button.textContent = 'START AR';
|
||||
sessionInit.domOverlay.root.style.display = 'none';
|
||||
|
||||
currentSession = null;
|
||||
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
button.style.display = '';
|
||||
|
||||
button.style.cursor = 'pointer';
|
||||
button.style.left = 'calc(50% - 50px)';
|
||||
button.style.width = '100px';
|
||||
|
||||
button.textContent = 'START AR';
|
||||
|
||||
button.onmouseenter = function () {
|
||||
|
||||
button.style.opacity = '1.0';
|
||||
|
||||
};
|
||||
|
||||
button.onmouseleave = function () {
|
||||
|
||||
button.style.opacity = '0.5';
|
||||
|
||||
};
|
||||
|
||||
button.onclick = function () {
|
||||
|
||||
if ( currentSession === null ) {
|
||||
|
||||
navigator.xr.requestSession( 'immersive-ar', sessionInit ).then( onSessionStarted );
|
||||
|
||||
} else {
|
||||
|
||||
currentSession.end();
|
||||
|
||||
if ( navigator.xr.offerSession !== undefined ) {
|
||||
|
||||
navigator.xr.offerSession( 'immersive-ar', sessionInit )
|
||||
.then( onSessionStarted )
|
||||
.catch( ( err ) => {
|
||||
|
||||
console.warn( err );
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
if ( navigator.xr.offerSession !== undefined ) {
|
||||
|
||||
navigator.xr.offerSession( 'immersive-ar', sessionInit )
|
||||
.then( onSessionStarted )
|
||||
.catch( ( err ) => {
|
||||
|
||||
console.warn( err );
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function disableButton() {
|
||||
|
||||
button.style.display = '';
|
||||
|
||||
button.style.cursor = 'auto';
|
||||
button.style.left = 'calc(50% - 75px)';
|
||||
button.style.width = '150px';
|
||||
|
||||
button.onmouseenter = null;
|
||||
button.onmouseleave = null;
|
||||
|
||||
button.onclick = null;
|
||||
|
||||
}
|
||||
|
||||
function showARNotSupported() {
|
||||
|
||||
disableButton();
|
||||
|
||||
button.textContent = 'AR NOT SUPPORTED';
|
||||
|
||||
}
|
||||
|
||||
function showARNotAllowed( exception ) {
|
||||
|
||||
disableButton();
|
||||
|
||||
console.warn( 'Exception when trying to call xr.isSessionSupported', exception );
|
||||
|
||||
button.textContent = 'AR NOT ALLOWED';
|
||||
|
||||
}
|
||||
|
||||
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 ) {
|
||||
|
||||
button.id = 'ARButton';
|
||||
button.style.display = 'none';
|
||||
|
||||
stylizeElement( button );
|
||||
|
||||
navigator.xr.isSessionSupported( 'immersive-ar' ).then( function ( supported ) {
|
||||
|
||||
supported ? showStartAR() : showARNotSupported();
|
||||
|
||||
} ).catch( showARNotAllowed );
|
||||
|
||||
return button;
|
||||
|
||||
} else {
|
||||
|
||||
const message = document.createElement( 'a' );
|
||||
|
||||
if ( window.isSecureContext === false ) {
|
||||
|
||||
message.href = document.location.href.replace( /^http:/, 'https:' );
|
||||
message.innerHTML = 'WEBXR NEEDS HTTPS'; // TODO Improve message
|
||||
|
||||
} else {
|
||||
|
||||
message.href = 'https://immersiveweb.dev/';
|
||||
message.innerHTML = 'WEBXR NOT AVAILABLE';
|
||||
|
||||
}
|
||||
|
||||
message.style.left = 'calc(50% - 90px)';
|
||||
message.style.width = '180px';
|
||||
message.style.textDecoration = 'none';
|
||||
|
||||
stylizeElement( message );
|
||||
|
||||
return message;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { ARButton };
|
110
dist/electron/static/sdk/three/jsm/webxr/OculusHandModel.js
vendored
Normal file
110
dist/electron/static/sdk/three/jsm/webxr/OculusHandModel.js
vendored
Normal file
@ -0,0 +1,110 @@
|
||||
import { Object3D, Sphere, Box3 } from 'three';
|
||||
import { XRHandMeshModel } from './XRHandMeshModel.js';
|
||||
|
||||
const TOUCH_RADIUS = 0.01;
|
||||
const POINTING_JOINT = 'index-finger-tip';
|
||||
|
||||
class OculusHandModel extends Object3D {
|
||||
|
||||
constructor( controller, loader = null, onLoad = null ) {
|
||||
|
||||
super();
|
||||
|
||||
this.controller = controller;
|
||||
this.motionController = null;
|
||||
this.envMap = null;
|
||||
this.loader = loader;
|
||||
this.onLoad = onLoad;
|
||||
|
||||
this.mesh = null;
|
||||
|
||||
controller.addEventListener( 'connected', ( event ) => {
|
||||
|
||||
const xrInputSource = event.data;
|
||||
|
||||
if ( xrInputSource.hand && ! this.motionController ) {
|
||||
|
||||
this.xrInputSource = xrInputSource;
|
||||
|
||||
this.motionController = new XRHandMeshModel( this, controller, this.path, xrInputSource.handedness, this.loader, this.onLoad );
|
||||
|
||||
}
|
||||
|
||||
} );
|
||||
|
||||
controller.addEventListener( 'disconnected', () => {
|
||||
|
||||
this.clear();
|
||||
this.motionController = null;
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
updateMatrixWorld( force ) {
|
||||
|
||||
super.updateMatrixWorld( force );
|
||||
|
||||
if ( this.motionController ) {
|
||||
|
||||
this.motionController.updateMesh();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
getPointerPosition() {
|
||||
|
||||
const indexFingerTip = this.controller.joints[ POINTING_JOINT ];
|
||||
if ( indexFingerTip ) {
|
||||
|
||||
return indexFingerTip.position;
|
||||
|
||||
} else {
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
intersectBoxObject( boxObject ) {
|
||||
|
||||
const pointerPosition = this.getPointerPosition();
|
||||
if ( pointerPosition ) {
|
||||
|
||||
const indexSphere = new Sphere( pointerPosition, TOUCH_RADIUS );
|
||||
const box = new Box3().setFromObject( boxObject );
|
||||
return indexSphere.intersectsBox( box );
|
||||
|
||||
} else {
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
checkButton( button ) {
|
||||
|
||||
if ( this.intersectBoxObject( button ) ) {
|
||||
|
||||
button.onPress();
|
||||
|
||||
} else {
|
||||
|
||||
button.onClear();
|
||||
|
||||
}
|
||||
|
||||
if ( button.isPressed() ) {
|
||||
|
||||
button.whilePressed();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { OculusHandModel };
|
413
dist/electron/static/sdk/three/jsm/webxr/OculusHandPointerModel.js
vendored
Normal file
413
dist/electron/static/sdk/three/jsm/webxr/OculusHandPointerModel.js
vendored
Normal file
@ -0,0 +1,413 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
const PINCH_MAX = 0.05;
|
||||
const PINCH_THRESHOLD = 0.02;
|
||||
const PINCH_MIN = 0.01;
|
||||
const POINTER_ADVANCE_MAX = 0.02;
|
||||
const POINTER_OPACITY_MAX = 1;
|
||||
const POINTER_OPACITY_MIN = 0.4;
|
||||
const POINTER_FRONT_RADIUS = 0.002;
|
||||
const POINTER_REAR_RADIUS = 0.01;
|
||||
const POINTER_REAR_RADIUS_MIN = 0.003;
|
||||
const POINTER_LENGTH = 0.035;
|
||||
const POINTER_SEGMENTS = 16;
|
||||
const POINTER_RINGS = 12;
|
||||
const POINTER_HEMISPHERE_ANGLE = 110;
|
||||
const YAXIS = /* @__PURE__ */ new THREE.Vector3( 0, 1, 0 );
|
||||
const ZAXIS = /* @__PURE__ */ new THREE.Vector3( 0, 0, 1 );
|
||||
|
||||
const CURSOR_RADIUS = 0.02;
|
||||
const CURSOR_MAX_DISTANCE = 1.5;
|
||||
|
||||
class OculusHandPointerModel extends THREE.Object3D {
|
||||
|
||||
constructor( hand, controller ) {
|
||||
|
||||
super();
|
||||
|
||||
this.hand = hand;
|
||||
this.controller = controller;
|
||||
|
||||
// Unused
|
||||
this.motionController = null;
|
||||
this.envMap = null;
|
||||
this.mesh = null;
|
||||
|
||||
this.pointerGeometry = null;
|
||||
this.pointerMesh = null;
|
||||
this.pointerObject = null;
|
||||
|
||||
this.pinched = false;
|
||||
this.attached = false;
|
||||
|
||||
this.cursorObject = null;
|
||||
|
||||
this.raycaster = null;
|
||||
|
||||
this._onConnected = this._onConnected.bind( this );
|
||||
this._onDisconnected = this._onDisconnected.bind( this );
|
||||
this.hand.addEventListener( 'connected', this._onConnected );
|
||||
this.hand.addEventListener( 'disconnected', this._onDisconnected );
|
||||
|
||||
}
|
||||
|
||||
_onConnected( event ) {
|
||||
|
||||
const xrInputSource = event.data;
|
||||
if ( xrInputSource.hand ) {
|
||||
|
||||
this.visible = true;
|
||||
this.xrInputSource = xrInputSource;
|
||||
|
||||
this.createPointer();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
_onDisconnected() {
|
||||
|
||||
this.visible = false;
|
||||
this.xrInputSource = null;
|
||||
|
||||
if ( this.pointerGeometry ) this.pointerGeometry.dispose();
|
||||
if ( this.pointerMesh && this.pointerMesh.material ) this.pointerMesh.material.dispose();
|
||||
|
||||
this.clear();
|
||||
|
||||
}
|
||||
|
||||
_drawVerticesRing( vertices, baseVector, ringIndex ) {
|
||||
|
||||
const segmentVector = baseVector.clone();
|
||||
for ( let i = 0; i < POINTER_SEGMENTS; i ++ ) {
|
||||
|
||||
segmentVector.applyAxisAngle( ZAXIS, ( Math.PI * 2 ) / POINTER_SEGMENTS );
|
||||
const vid = ringIndex * POINTER_SEGMENTS + i;
|
||||
vertices[ 3 * vid ] = segmentVector.x;
|
||||
vertices[ 3 * vid + 1 ] = segmentVector.y;
|
||||
vertices[ 3 * vid + 2 ] = segmentVector.z;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
_updatePointerVertices( rearRadius ) {
|
||||
|
||||
const vertices = this.pointerGeometry.attributes.position.array;
|
||||
// first ring for front face
|
||||
const frontFaceBase = new THREE.Vector3(
|
||||
POINTER_FRONT_RADIUS,
|
||||
0,
|
||||
- 1 * ( POINTER_LENGTH - rearRadius )
|
||||
);
|
||||
this._drawVerticesRing( vertices, frontFaceBase, 0 );
|
||||
|
||||
// rings for rear hemisphere
|
||||
const rearBase = new THREE.Vector3(
|
||||
Math.sin( ( Math.PI * POINTER_HEMISPHERE_ANGLE ) / 180 ) * rearRadius,
|
||||
Math.cos( ( Math.PI * POINTER_HEMISPHERE_ANGLE ) / 180 ) * rearRadius,
|
||||
0
|
||||
);
|
||||
for ( let i = 0; i < POINTER_RINGS; i ++ ) {
|
||||
|
||||
this._drawVerticesRing( vertices, rearBase, i + 1 );
|
||||
rearBase.applyAxisAngle(
|
||||
YAXIS,
|
||||
( Math.PI * POINTER_HEMISPHERE_ANGLE ) / 180 / ( POINTER_RINGS * - 2 )
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
// front and rear face center vertices
|
||||
const frontCenterIndex = POINTER_SEGMENTS * ( 1 + POINTER_RINGS );
|
||||
const rearCenterIndex = POINTER_SEGMENTS * ( 1 + POINTER_RINGS ) + 1;
|
||||
const frontCenter = new THREE.Vector3(
|
||||
0,
|
||||
0,
|
||||
- 1 * ( POINTER_LENGTH - rearRadius )
|
||||
);
|
||||
vertices[ frontCenterIndex * 3 ] = frontCenter.x;
|
||||
vertices[ frontCenterIndex * 3 + 1 ] = frontCenter.y;
|
||||
vertices[ frontCenterIndex * 3 + 2 ] = frontCenter.z;
|
||||
const rearCenter = new THREE.Vector3( 0, 0, rearRadius );
|
||||
vertices[ rearCenterIndex * 3 ] = rearCenter.x;
|
||||
vertices[ rearCenterIndex * 3 + 1 ] = rearCenter.y;
|
||||
vertices[ rearCenterIndex * 3 + 2 ] = rearCenter.z;
|
||||
|
||||
this.pointerGeometry.setAttribute(
|
||||
'position',
|
||||
new THREE.Float32BufferAttribute( vertices, 3 )
|
||||
);
|
||||
// verticesNeedUpdate = true;
|
||||
|
||||
}
|
||||
|
||||
createPointer() {
|
||||
|
||||
let i, j;
|
||||
const vertices = new Array(
|
||||
( ( POINTER_RINGS + 1 ) * POINTER_SEGMENTS + 2 ) * 3
|
||||
).fill( 0 );
|
||||
// const vertices = [];
|
||||
const indices = [];
|
||||
this.pointerGeometry = new THREE.BufferGeometry();
|
||||
|
||||
this.pointerGeometry.setAttribute(
|
||||
'position',
|
||||
new THREE.Float32BufferAttribute( vertices, 3 )
|
||||
);
|
||||
|
||||
this._updatePointerVertices( POINTER_REAR_RADIUS );
|
||||
|
||||
// construct faces to connect rings
|
||||
for ( i = 0; i < POINTER_RINGS; i ++ ) {
|
||||
|
||||
for ( j = 0; j < POINTER_SEGMENTS - 1; j ++ ) {
|
||||
|
||||
indices.push(
|
||||
i * POINTER_SEGMENTS + j,
|
||||
i * POINTER_SEGMENTS + j + 1,
|
||||
( i + 1 ) * POINTER_SEGMENTS + j
|
||||
);
|
||||
indices.push(
|
||||
i * POINTER_SEGMENTS + j + 1,
|
||||
( i + 1 ) * POINTER_SEGMENTS + j + 1,
|
||||
( i + 1 ) * POINTER_SEGMENTS + j
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
indices.push(
|
||||
( i + 1 ) * POINTER_SEGMENTS - 1,
|
||||
i * POINTER_SEGMENTS,
|
||||
( i + 2 ) * POINTER_SEGMENTS - 1
|
||||
);
|
||||
indices.push(
|
||||
i * POINTER_SEGMENTS,
|
||||
( i + 1 ) * POINTER_SEGMENTS,
|
||||
( i + 2 ) * POINTER_SEGMENTS - 1
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
// construct front and rear face
|
||||
const frontCenterIndex = POINTER_SEGMENTS * ( 1 + POINTER_RINGS );
|
||||
const rearCenterIndex = POINTER_SEGMENTS * ( 1 + POINTER_RINGS ) + 1;
|
||||
|
||||
for ( i = 0; i < POINTER_SEGMENTS - 1; i ++ ) {
|
||||
|
||||
indices.push( frontCenterIndex, i + 1, i );
|
||||
indices.push(
|
||||
rearCenterIndex,
|
||||
i + POINTER_SEGMENTS * POINTER_RINGS,
|
||||
i + POINTER_SEGMENTS * POINTER_RINGS + 1
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
indices.push( frontCenterIndex, 0, POINTER_SEGMENTS - 1 );
|
||||
indices.push(
|
||||
rearCenterIndex,
|
||||
POINTER_SEGMENTS * ( POINTER_RINGS + 1 ) - 1,
|
||||
POINTER_SEGMENTS * POINTER_RINGS
|
||||
);
|
||||
|
||||
const material = new THREE.MeshBasicMaterial();
|
||||
material.transparent = true;
|
||||
material.opacity = POINTER_OPACITY_MIN;
|
||||
|
||||
this.pointerGeometry.setIndex( indices );
|
||||
|
||||
this.pointerMesh = new THREE.Mesh( this.pointerGeometry, material );
|
||||
|
||||
this.pointerMesh.position.set( 0, 0, - 1 * POINTER_REAR_RADIUS );
|
||||
this.pointerObject = new THREE.Object3D();
|
||||
this.pointerObject.add( this.pointerMesh );
|
||||
|
||||
this.raycaster = new THREE.Raycaster();
|
||||
|
||||
// create cursor
|
||||
const cursorGeometry = new THREE.SphereGeometry( CURSOR_RADIUS, 10, 10 );
|
||||
const cursorMaterial = new THREE.MeshBasicMaterial();
|
||||
cursorMaterial.transparent = true;
|
||||
cursorMaterial.opacity = POINTER_OPACITY_MIN;
|
||||
|
||||
this.cursorObject = new THREE.Mesh( cursorGeometry, cursorMaterial );
|
||||
this.pointerObject.add( this.cursorObject );
|
||||
|
||||
this.add( this.pointerObject );
|
||||
|
||||
}
|
||||
|
||||
_updateRaycaster() {
|
||||
|
||||
if ( this.raycaster ) {
|
||||
|
||||
const pointerMatrix = this.pointerObject.matrixWorld;
|
||||
const tempMatrix = new THREE.Matrix4();
|
||||
tempMatrix.identity().extractRotation( pointerMatrix );
|
||||
this.raycaster.ray.origin.setFromMatrixPosition( pointerMatrix );
|
||||
this.raycaster.ray.direction.set( 0, 0, - 1 ).applyMatrix4( tempMatrix );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
_updatePointer() {
|
||||
|
||||
this.pointerObject.visible = this.controller.visible;
|
||||
const indexTip = this.hand.joints[ 'index-finger-tip' ];
|
||||
const thumbTip = this.hand.joints[ 'thumb-tip' ];
|
||||
const distance = indexTip.position.distanceTo( thumbTip.position );
|
||||
const position = indexTip.position
|
||||
.clone()
|
||||
.add( thumbTip.position )
|
||||
.multiplyScalar( 0.5 );
|
||||
this.pointerObject.position.copy( position );
|
||||
this.pointerObject.quaternion.copy( this.controller.quaternion );
|
||||
|
||||
this.pinched = distance <= PINCH_THRESHOLD;
|
||||
|
||||
const pinchScale = ( distance - PINCH_MIN ) / ( PINCH_MAX - PINCH_MIN );
|
||||
const focusScale = ( distance - PINCH_MIN ) / ( PINCH_THRESHOLD - PINCH_MIN );
|
||||
if ( pinchScale > 1 ) {
|
||||
|
||||
this._updatePointerVertices( POINTER_REAR_RADIUS );
|
||||
this.pointerMesh.position.set( 0, 0, - 1 * POINTER_REAR_RADIUS );
|
||||
this.pointerMesh.material.opacity = POINTER_OPACITY_MIN;
|
||||
|
||||
} else if ( pinchScale > 0 ) {
|
||||
|
||||
const rearRadius =
|
||||
( POINTER_REAR_RADIUS - POINTER_REAR_RADIUS_MIN ) * pinchScale +
|
||||
POINTER_REAR_RADIUS_MIN;
|
||||
this._updatePointerVertices( rearRadius );
|
||||
if ( focusScale < 1 ) {
|
||||
|
||||
this.pointerMesh.position.set(
|
||||
0,
|
||||
0,
|
||||
- 1 * rearRadius - ( 1 - focusScale ) * POINTER_ADVANCE_MAX
|
||||
);
|
||||
this.pointerMesh.material.opacity =
|
||||
POINTER_OPACITY_MIN +
|
||||
( 1 - focusScale ) * ( POINTER_OPACITY_MAX - POINTER_OPACITY_MIN );
|
||||
|
||||
} else {
|
||||
|
||||
this.pointerMesh.position.set( 0, 0, - 1 * rearRadius );
|
||||
this.pointerMesh.material.opacity = POINTER_OPACITY_MIN;
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
this._updatePointerVertices( POINTER_REAR_RADIUS_MIN );
|
||||
this.pointerMesh.position.set(
|
||||
0,
|
||||
0,
|
||||
- 1 * POINTER_REAR_RADIUS_MIN - POINTER_ADVANCE_MAX
|
||||
);
|
||||
this.pointerMesh.material.opacity = POINTER_OPACITY_MAX;
|
||||
|
||||
}
|
||||
|
||||
this.cursorObject.material.opacity = this.pointerMesh.material.opacity;
|
||||
|
||||
}
|
||||
|
||||
updateMatrixWorld( force ) {
|
||||
|
||||
super.updateMatrixWorld( force );
|
||||
if ( this.pointerGeometry ) {
|
||||
|
||||
this._updatePointer();
|
||||
this._updateRaycaster();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
isPinched() {
|
||||
|
||||
return this.pinched;
|
||||
|
||||
}
|
||||
|
||||
setAttached( attached ) {
|
||||
|
||||
this.attached = attached;
|
||||
|
||||
}
|
||||
|
||||
isAttached() {
|
||||
|
||||
return this.attached;
|
||||
|
||||
}
|
||||
|
||||
intersectObject( object, recursive = true ) {
|
||||
|
||||
if ( this.raycaster ) {
|
||||
|
||||
return this.raycaster.intersectObject( object, recursive );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
intersectObjects( objects, recursive = true ) {
|
||||
|
||||
if ( this.raycaster ) {
|
||||
|
||||
return this.raycaster.intersectObjects( objects, recursive );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
checkIntersections( objects, recursive = false ) {
|
||||
|
||||
if ( this.raycaster && ! this.attached ) {
|
||||
|
||||
const intersections = this.raycaster.intersectObjects( objects, recursive );
|
||||
const direction = new THREE.Vector3( 0, 0, - 1 );
|
||||
if ( intersections.length > 0 ) {
|
||||
|
||||
const intersection = intersections[ 0 ];
|
||||
const distance = intersection.distance;
|
||||
this.cursorObject.position.copy( direction.multiplyScalar( distance ) );
|
||||
|
||||
} else {
|
||||
|
||||
this.cursorObject.position.copy( direction.multiplyScalar( CURSOR_MAX_DISTANCE ) );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
setCursor( distance ) {
|
||||
|
||||
const direction = new THREE.Vector3( 0, 0, - 1 );
|
||||
if ( this.raycaster && ! this.attached ) {
|
||||
|
||||
this.cursorObject.position.copy( direction.multiplyScalar( distance ) );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dispose() {
|
||||
|
||||
this._onDisconnected();
|
||||
this.hand.removeEventListener( 'connected', this._onConnected );
|
||||
this.hand.removeEventListener( 'disconnected', this._onDisconnected );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { OculusHandPointerModel };
|
38
dist/electron/static/sdk/three/jsm/webxr/Text2D.js
vendored
Normal file
38
dist/electron/static/sdk/three/jsm/webxr/Text2D.js
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
function createText( message, height ) {
|
||||
|
||||
const canvas = document.createElement( 'canvas' );
|
||||
const context = canvas.getContext( '2d' );
|
||||
let metrics = null;
|
||||
const textHeight = 100;
|
||||
context.font = 'normal ' + textHeight + 'px Arial';
|
||||
metrics = context.measureText( message );
|
||||
const textWidth = metrics.width;
|
||||
canvas.width = textWidth;
|
||||
canvas.height = textHeight;
|
||||
context.font = 'normal ' + textHeight + 'px Arial';
|
||||
context.textAlign = 'center';
|
||||
context.textBaseline = 'middle';
|
||||
context.fillStyle = '#ffffff';
|
||||
context.fillText( message, textWidth / 2, textHeight / 2 );
|
||||
|
||||
const texture = new THREE.Texture( canvas );
|
||||
texture.needsUpdate = true;
|
||||
|
||||
const material = new THREE.MeshBasicMaterial( {
|
||||
color: 0xffffff,
|
||||
side: THREE.DoubleSide,
|
||||
map: texture,
|
||||
transparent: true,
|
||||
} );
|
||||
const geometry = new THREE.PlaneGeometry(
|
||||
( height * textWidth ) / textHeight,
|
||||
height
|
||||
);
|
||||
const plane = new THREE.Mesh( geometry, material );
|
||||
return plane;
|
||||
|
||||
}
|
||||
|
||||
export { createText };
|
233
dist/electron/static/sdk/three/jsm/webxr/VRButton.js
vendored
Normal file
233
dist/electron/static/sdk/three/jsm/webxr/VRButton.js
vendored
Normal file
@ -0,0 +1,233 @@
|
||||
class VRButton {
|
||||
|
||||
static createButton( renderer, sessionInit = {} ) {
|
||||
|
||||
const button = document.createElement( 'button' );
|
||||
|
||||
function showEnterVR( /*device*/ ) {
|
||||
|
||||
let currentSession = null;
|
||||
|
||||
async function onSessionStarted( session ) {
|
||||
|
||||
session.addEventListener( 'end', onSessionEnded );
|
||||
|
||||
await renderer.xr.setSession( session );
|
||||
button.textContent = 'EXIT VR';
|
||||
|
||||
currentSession = session;
|
||||
|
||||
}
|
||||
|
||||
function onSessionEnded( /*event*/ ) {
|
||||
|
||||
currentSession.removeEventListener( 'end', onSessionEnded );
|
||||
|
||||
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';
|
||||
|
||||
// WebXR's requestReferenceSpace only works if the corresponding feature
|
||||
// was requested at session creation time. For simplicity, just ask for
|
||||
// the interesting ones as optional features, but be aware that the
|
||||
// requestReferenceSpace call will fail if it turns out to be unavailable.
|
||||
// ('local' is always available for immersive sessions and doesn't need to
|
||||
// be requested separately.)
|
||||
|
||||
const sessionOptions = {
|
||||
...sessionInit,
|
||||
optionalFeatures: [
|
||||
'local-floor',
|
||||
'bounded-floor',
|
||||
'layers',
|
||||
...( sessionInit.optionalFeatures || [] )
|
||||
],
|
||||
};
|
||||
|
||||
button.onmouseenter = function () {
|
||||
|
||||
button.style.opacity = '1.0';
|
||||
|
||||
};
|
||||
|
||||
button.onmouseleave = function () {
|
||||
|
||||
button.style.opacity = '0.5';
|
||||
|
||||
};
|
||||
|
||||
button.onclick = function () {
|
||||
|
||||
if ( currentSession === null ) {
|
||||
|
||||
navigator.xr.requestSession( 'immersive-vr', sessionOptions ).then( onSessionStarted );
|
||||
|
||||
} else {
|
||||
|
||||
currentSession.end();
|
||||
|
||||
if ( navigator.xr.offerSession !== undefined ) {
|
||||
|
||||
navigator.xr.offerSession( 'immersive-vr', sessionOptions )
|
||||
.then( onSessionStarted )
|
||||
.catch( ( err ) => {
|
||||
|
||||
console.warn( err );
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
if ( navigator.xr.offerSession !== undefined ) {
|
||||
|
||||
navigator.xr.offerSession( 'immersive-vr', sessionOptions )
|
||||
.then( onSessionStarted )
|
||||
.catch( ( err ) => {
|
||||
|
||||
console.warn( err );
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function disableButton() {
|
||||
|
||||
button.style.display = '';
|
||||
|
||||
button.style.cursor = 'auto';
|
||||
button.style.left = 'calc(50% - 75px)';
|
||||
button.style.width = '150px';
|
||||
|
||||
button.onmouseenter = null;
|
||||
button.onmouseleave = null;
|
||||
|
||||
button.onclick = null;
|
||||
|
||||
}
|
||||
|
||||
function showWebXRNotFound() {
|
||||
|
||||
disableButton();
|
||||
|
||||
button.textContent = 'VR NOT SUPPORTED';
|
||||
|
||||
}
|
||||
|
||||
function showVRNotAllowed( exception ) {
|
||||
|
||||
disableButton();
|
||||
|
||||
console.warn( 'Exception when trying to call xr.isSessionSupported', exception );
|
||||
|
||||
button.textContent = 'VR NOT ALLOWED';
|
||||
|
||||
}
|
||||
|
||||
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 ) {
|
||||
|
||||
button.id = 'VRButton';
|
||||
button.style.display = 'none';
|
||||
|
||||
stylizeElement( button );
|
||||
|
||||
navigator.xr.isSessionSupported( 'immersive-vr' ).then( function ( supported ) {
|
||||
|
||||
supported ? showEnterVR() : showWebXRNotFound();
|
||||
|
||||
if ( supported && VRButton.xrSessionIsGranted ) {
|
||||
|
||||
button.click();
|
||||
|
||||
}
|
||||
|
||||
} ).catch( showVRNotAllowed );
|
||||
|
||||
return button;
|
||||
|
||||
} else {
|
||||
|
||||
const message = document.createElement( 'a' );
|
||||
|
||||
if ( window.isSecureContext === false ) {
|
||||
|
||||
message.href = document.location.href.replace( /^http:/, 'https:' );
|
||||
message.innerHTML = 'WEBXR NEEDS HTTPS'; // TODO Improve message
|
||||
|
||||
} else {
|
||||
|
||||
message.href = 'https://immersiveweb.dev/';
|
||||
message.innerHTML = 'WEBXR NOT AVAILABLE';
|
||||
|
||||
}
|
||||
|
||||
message.style.left = 'calc(50% - 90px)';
|
||||
message.style.width = '180px';
|
||||
message.style.textDecoration = 'none';
|
||||
|
||||
stylizeElement( message );
|
||||
|
||||
return message;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static registerSessionGrantedListener() {
|
||||
|
||||
if ( typeof navigator !== 'undefined' && 'xr' in navigator ) {
|
||||
|
||||
// WebXRViewer (based on Firefox) has a bug where addEventListener
|
||||
// throws a silent exception and aborts execution entirely.
|
||||
if ( /WebXRViewer\//i.test( navigator.userAgent ) ) return;
|
||||
|
||||
navigator.xr.addEventListener( 'sessiongranted', () => {
|
||||
|
||||
VRButton.xrSessionIsGranted = true;
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
VRButton.xrSessionIsGranted = false;
|
||||
VRButton.registerSessionGrantedListener();
|
||||
|
||||
export { VRButton };
|
223
dist/electron/static/sdk/three/jsm/webxr/XRButton.js
vendored
Normal file
223
dist/electron/static/sdk/three/jsm/webxr/XRButton.js
vendored
Normal file
@ -0,0 +1,223 @@
|
||||
class XRButton {
|
||||
|
||||
static createButton( renderer, sessionInit = {} ) {
|
||||
|
||||
const button = document.createElement( 'button' );
|
||||
|
||||
function showStartXR( mode ) {
|
||||
|
||||
let currentSession = null;
|
||||
|
||||
async function onSessionStarted( session ) {
|
||||
|
||||
session.addEventListener( 'end', onSessionEnded );
|
||||
|
||||
await renderer.xr.setSession( session );
|
||||
|
||||
button.textContent = 'STOP XR';
|
||||
|
||||
currentSession = session;
|
||||
|
||||
}
|
||||
|
||||
function onSessionEnded( /*event*/ ) {
|
||||
|
||||
currentSession.removeEventListener( 'end', onSessionEnded );
|
||||
|
||||
button.textContent = 'START XR';
|
||||
|
||||
currentSession = null;
|
||||
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
button.style.display = '';
|
||||
|
||||
button.style.cursor = 'pointer';
|
||||
button.style.left = 'calc(50% - 50px)';
|
||||
button.style.width = '100px';
|
||||
|
||||
button.textContent = 'START XR';
|
||||
|
||||
const sessionOptions = {
|
||||
...sessionInit,
|
||||
optionalFeatures: [
|
||||
'local-floor',
|
||||
'bounded-floor',
|
||||
'layers',
|
||||
...( sessionInit.optionalFeatures || [] )
|
||||
],
|
||||
};
|
||||
|
||||
button.onmouseenter = function () {
|
||||
|
||||
button.style.opacity = '1.0';
|
||||
|
||||
};
|
||||
|
||||
button.onmouseleave = function () {
|
||||
|
||||
button.style.opacity = '0.5';
|
||||
|
||||
};
|
||||
|
||||
button.onclick = function () {
|
||||
|
||||
if ( currentSession === null ) {
|
||||
|
||||
navigator.xr.requestSession( mode, sessionOptions )
|
||||
.then( onSessionStarted );
|
||||
|
||||
} else {
|
||||
|
||||
currentSession.end();
|
||||
|
||||
if ( navigator.xr.offerSession !== undefined ) {
|
||||
|
||||
navigator.xr.offerSession( mode, sessionOptions )
|
||||
.then( onSessionStarted )
|
||||
.catch( ( err ) => {
|
||||
|
||||
console.warn( err );
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
if ( navigator.xr.offerSession !== undefined ) {
|
||||
|
||||
navigator.xr.offerSession( mode, sessionOptions )
|
||||
.then( onSessionStarted )
|
||||
.catch( ( err ) => {
|
||||
|
||||
console.warn( err );
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function disableButton() {
|
||||
|
||||
button.style.display = '';
|
||||
|
||||
button.style.cursor = 'auto';
|
||||
button.style.left = 'calc(50% - 75px)';
|
||||
button.style.width = '150px';
|
||||
|
||||
button.onmouseenter = null;
|
||||
button.onmouseleave = null;
|
||||
|
||||
button.onclick = null;
|
||||
|
||||
}
|
||||
|
||||
function showXRNotSupported() {
|
||||
|
||||
disableButton();
|
||||
|
||||
button.textContent = 'XR NOT SUPPORTED';
|
||||
|
||||
}
|
||||
|
||||
function showXRNotAllowed( exception ) {
|
||||
|
||||
disableButton();
|
||||
|
||||
console.warn( 'Exception when trying to call xr.isSessionSupported', exception );
|
||||
|
||||
button.textContent = 'XR NOT ALLOWED';
|
||||
|
||||
}
|
||||
|
||||
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 ) {
|
||||
|
||||
button.id = 'XRButton';
|
||||
button.style.display = 'none';
|
||||
|
||||
stylizeElement( button );
|
||||
|
||||
navigator.xr.isSessionSupported( 'immersive-ar' )
|
||||
.then( function ( supported ) {
|
||||
|
||||
if ( supported ) {
|
||||
|
||||
showStartXR( 'immersive-ar' );
|
||||
|
||||
} else {
|
||||
|
||||
navigator.xr.isSessionSupported( 'immersive-vr' )
|
||||
.then( function ( supported ) {
|
||||
|
||||
if ( supported ) {
|
||||
|
||||
showStartXR( 'immersive-vr' );
|
||||
|
||||
} else {
|
||||
|
||||
showXRNotSupported();
|
||||
|
||||
}
|
||||
|
||||
} ).catch( showXRNotAllowed );
|
||||
|
||||
}
|
||||
|
||||
} ).catch( showXRNotAllowed );
|
||||
|
||||
return button;
|
||||
|
||||
} else {
|
||||
|
||||
const message = document.createElement( 'a' );
|
||||
|
||||
if ( window.isSecureContext === false ) {
|
||||
|
||||
message.href = document.location.href.replace( /^http:/, 'https:' );
|
||||
message.innerHTML = 'WEBXR NEEDS HTTPS'; // TODO Improve message
|
||||
|
||||
} else {
|
||||
|
||||
message.href = 'https://immersiveweb.dev/';
|
||||
message.innerHTML = 'WEBXR NOT AVAILABLE';
|
||||
|
||||
}
|
||||
|
||||
message.style.left = 'calc(50% - 90px)';
|
||||
message.style.width = '180px';
|
||||
message.style.textDecoration = 'none';
|
||||
|
||||
stylizeElement( message );
|
||||
|
||||
return message;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { XRButton };
|
312
dist/electron/static/sdk/three/jsm/webxr/XRControllerModelFactory.js
vendored
Normal file
312
dist/electron/static/sdk/three/jsm/webxr/XRControllerModelFactory.js
vendored
Normal file
@ -0,0 +1,312 @@
|
||||
import {
|
||||
Mesh,
|
||||
MeshBasicMaterial,
|
||||
Object3D,
|
||||
SphereGeometry,
|
||||
} from 'three';
|
||||
|
||||
import { GLTFLoader } from '../loaders/GLTFLoader.js';
|
||||
|
||||
import {
|
||||
Constants as MotionControllerConstants,
|
||||
fetchProfile,
|
||||
MotionController
|
||||
} from '../libs/motion-controllers.module.js';
|
||||
|
||||
const DEFAULT_PROFILES_PATH = 'https://cdn.jsdelivr.net/npm/@webxr-input-profiles/assets@1.0/dist/profiles';
|
||||
const DEFAULT_PROFILE = 'generic-trigger';
|
||||
|
||||
class XRControllerModel extends Object3D {
|
||||
|
||||
constructor() {
|
||||
|
||||
super();
|
||||
|
||||
this.motionController = null;
|
||||
this.envMap = null;
|
||||
|
||||
}
|
||||
|
||||
setEnvironmentMap( envMap ) {
|
||||
|
||||
if ( this.envMap == envMap ) {
|
||||
|
||||
return this;
|
||||
|
||||
}
|
||||
|
||||
this.envMap = envMap;
|
||||
this.traverse( ( child ) => {
|
||||
|
||||
if ( child.isMesh ) {
|
||||
|
||||
child.material.envMap = this.envMap;
|
||||
child.material.needsUpdate = true;
|
||||
|
||||
}
|
||||
|
||||
} );
|
||||
|
||||
return this;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Polls data from the XRInputSource and updates the model's components to match
|
||||
* the real world data
|
||||
*/
|
||||
updateMatrixWorld( force ) {
|
||||
|
||||
super.updateMatrixWorld( force );
|
||||
|
||||
if ( ! this.motionController ) return;
|
||||
|
||||
// Cause the MotionController to poll the Gamepad for data
|
||||
this.motionController.updateFromGamepad();
|
||||
|
||||
// Update the 3D model to reflect the button, thumbstick, and touchpad state
|
||||
Object.values( this.motionController.components ).forEach( ( component ) => {
|
||||
|
||||
// Update node data based on the visual responses' current states
|
||||
Object.values( component.visualResponses ).forEach( ( visualResponse ) => {
|
||||
|
||||
const { valueNode, minNode, maxNode, value, valueNodeProperty } = visualResponse;
|
||||
|
||||
// Skip if the visual response node is not found. No error is needed,
|
||||
// because it will have been reported at load time.
|
||||
if ( ! valueNode ) return;
|
||||
|
||||
// Calculate the new properties based on the weight supplied
|
||||
if ( valueNodeProperty === MotionControllerConstants.VisualResponseProperty.VISIBILITY ) {
|
||||
|
||||
valueNode.visible = value;
|
||||
|
||||
} else if ( valueNodeProperty === MotionControllerConstants.VisualResponseProperty.TRANSFORM ) {
|
||||
|
||||
valueNode.quaternion.slerpQuaternions(
|
||||
minNode.quaternion,
|
||||
maxNode.quaternion,
|
||||
value
|
||||
);
|
||||
|
||||
valueNode.position.lerpVectors(
|
||||
minNode.position,
|
||||
maxNode.position,
|
||||
value
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
} );
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Walks the model's tree to find the nodes needed to animate the components and
|
||||
* saves them to the motionContoller components for use in the frame loop. When
|
||||
* touchpads are found, attaches a touch dot to them.
|
||||
*/
|
||||
function findNodes( motionController, scene ) {
|
||||
|
||||
// Loop through the components and find the nodes needed for each components' visual responses
|
||||
Object.values( motionController.components ).forEach( ( component ) => {
|
||||
|
||||
const { type, touchPointNodeName, visualResponses } = component;
|
||||
|
||||
if ( type === MotionControllerConstants.ComponentType.TOUCHPAD ) {
|
||||
|
||||
component.touchPointNode = scene.getObjectByName( touchPointNodeName );
|
||||
if ( component.touchPointNode ) {
|
||||
|
||||
// Attach a touch dot to the touchpad.
|
||||
const sphereGeometry = new SphereGeometry( 0.001 );
|
||||
const material = new MeshBasicMaterial( { color: 0x0000FF } );
|
||||
const sphere = new Mesh( sphereGeometry, material );
|
||||
component.touchPointNode.add( sphere );
|
||||
|
||||
} else {
|
||||
|
||||
console.warn( `Could not find touch dot, ${component.touchPointNodeName}, in touchpad component ${component.id}` );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Loop through all the visual responses to be applied to this component
|
||||
Object.values( visualResponses ).forEach( ( visualResponse ) => {
|
||||
|
||||
const { valueNodeName, minNodeName, maxNodeName, valueNodeProperty } = visualResponse;
|
||||
|
||||
// If animating a transform, find the two nodes to be interpolated between.
|
||||
if ( valueNodeProperty === MotionControllerConstants.VisualResponseProperty.TRANSFORM ) {
|
||||
|
||||
visualResponse.minNode = scene.getObjectByName( minNodeName );
|
||||
visualResponse.maxNode = scene.getObjectByName( maxNodeName );
|
||||
|
||||
// If the extents cannot be found, skip this animation
|
||||
if ( ! visualResponse.minNode ) {
|
||||
|
||||
console.warn( `Could not find ${minNodeName} in the model` );
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
if ( ! visualResponse.maxNode ) {
|
||||
|
||||
console.warn( `Could not find ${maxNodeName} in the model` );
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// If the target node cannot be found, skip this animation
|
||||
visualResponse.valueNode = scene.getObjectByName( valueNodeName );
|
||||
if ( ! visualResponse.valueNode ) {
|
||||
|
||||
console.warn( `Could not find ${valueNodeName} in the model` );
|
||||
|
||||
}
|
||||
|
||||
} );
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
function addAssetSceneToControllerModel( controllerModel, scene ) {
|
||||
|
||||
// Find the nodes needed for animation and cache them on the motionController.
|
||||
findNodes( controllerModel.motionController, scene );
|
||||
|
||||
// Apply any environment map that the mesh already has set.
|
||||
if ( controllerModel.envMap ) {
|
||||
|
||||
scene.traverse( ( child ) => {
|
||||
|
||||
if ( child.isMesh ) {
|
||||
|
||||
child.material.envMap = controllerModel.envMap;
|
||||
child.material.needsUpdate = true;
|
||||
|
||||
}
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
// Add the glTF scene to the controllerModel.
|
||||
controllerModel.add( scene );
|
||||
|
||||
}
|
||||
|
||||
class XRControllerModelFactory {
|
||||
|
||||
constructor( gltfLoader = null, onLoad = null ) {
|
||||
|
||||
this.gltfLoader = gltfLoader;
|
||||
this.path = DEFAULT_PROFILES_PATH;
|
||||
this._assetCache = {};
|
||||
this.onLoad = onLoad;
|
||||
|
||||
// If a GLTFLoader wasn't supplied to the constructor create a new one.
|
||||
if ( ! this.gltfLoader ) {
|
||||
|
||||
this.gltfLoader = new GLTFLoader();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
setPath( path ) {
|
||||
|
||||
this.path = path;
|
||||
|
||||
return this;
|
||||
|
||||
}
|
||||
|
||||
createControllerModel( controller ) {
|
||||
|
||||
const controllerModel = new XRControllerModel();
|
||||
let scene = null;
|
||||
|
||||
controller.addEventListener( 'connected', ( event ) => {
|
||||
|
||||
const xrInputSource = event.data;
|
||||
|
||||
if ( xrInputSource.targetRayMode !== 'tracked-pointer' || ! xrInputSource.gamepad ) return;
|
||||
|
||||
fetchProfile( xrInputSource, this.path, DEFAULT_PROFILE ).then( ( { profile, assetPath } ) => {
|
||||
|
||||
controllerModel.motionController = new MotionController(
|
||||
xrInputSource,
|
||||
profile,
|
||||
assetPath
|
||||
);
|
||||
|
||||
const cachedAsset = this._assetCache[ controllerModel.motionController.assetUrl ];
|
||||
if ( cachedAsset ) {
|
||||
|
||||
scene = cachedAsset.scene.clone();
|
||||
|
||||
addAssetSceneToControllerModel( controllerModel, scene );
|
||||
|
||||
if ( this.onLoad ) this.onLoad( scene );
|
||||
|
||||
} else {
|
||||
|
||||
if ( ! this.gltfLoader ) {
|
||||
|
||||
throw new Error( 'GLTFLoader not set.' );
|
||||
|
||||
}
|
||||
|
||||
this.gltfLoader.setPath( '' );
|
||||
this.gltfLoader.load( controllerModel.motionController.assetUrl, ( asset ) => {
|
||||
|
||||
this._assetCache[ controllerModel.motionController.assetUrl ] = asset;
|
||||
|
||||
scene = asset.scene.clone();
|
||||
|
||||
addAssetSceneToControllerModel( controllerModel, scene );
|
||||
|
||||
if ( this.onLoad ) this.onLoad( scene );
|
||||
|
||||
},
|
||||
null,
|
||||
() => {
|
||||
|
||||
throw new Error( `Asset ${controllerModel.motionController.assetUrl} missing or malformed.` );
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
} ).catch( ( err ) => {
|
||||
|
||||
console.warn( err );
|
||||
|
||||
} );
|
||||
|
||||
} );
|
||||
|
||||
controller.addEventListener( 'disconnected', () => {
|
||||
|
||||
controllerModel.motionController = null;
|
||||
controllerModel.remove( scene );
|
||||
scene = null;
|
||||
|
||||
} );
|
||||
|
||||
return controllerModel;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { XRControllerModelFactory };
|
223
dist/electron/static/sdk/three/jsm/webxr/XREstimatedLight.js
vendored
Normal file
223
dist/electron/static/sdk/three/jsm/webxr/XREstimatedLight.js
vendored
Normal file
@ -0,0 +1,223 @@
|
||||
import {
|
||||
DirectionalLight,
|
||||
Group,
|
||||
LightProbe,
|
||||
WebGLCubeRenderTarget
|
||||
} from 'three';
|
||||
|
||||
class SessionLightProbe {
|
||||
|
||||
constructor( xrLight, renderer, lightProbe, environmentEstimation, estimationStartCallback ) {
|
||||
|
||||
this.xrLight = xrLight;
|
||||
this.renderer = renderer;
|
||||
this.lightProbe = lightProbe;
|
||||
this.xrWebGLBinding = null;
|
||||
this.estimationStartCallback = estimationStartCallback;
|
||||
this.frameCallback = this.onXRFrame.bind( this );
|
||||
|
||||
const session = renderer.xr.getSession();
|
||||
|
||||
// If the XRWebGLBinding class is available then we can also query an
|
||||
// estimated reflection cube map.
|
||||
if ( environmentEstimation && 'XRWebGLBinding' in window ) {
|
||||
|
||||
// This is the simplest way I know of to initialize a WebGL cubemap in Three.
|
||||
const cubeRenderTarget = new WebGLCubeRenderTarget( 16 );
|
||||
xrLight.environment = cubeRenderTarget.texture;
|
||||
|
||||
const gl = renderer.getContext();
|
||||
|
||||
// Ensure that we have any extensions needed to use the preferred cube map format.
|
||||
switch ( session.preferredReflectionFormat ) {
|
||||
|
||||
case 'srgba8':
|
||||
gl.getExtension( 'EXT_sRGB' );
|
||||
break;
|
||||
|
||||
case 'rgba16f':
|
||||
gl.getExtension( 'OES_texture_half_float' );
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
this.xrWebGLBinding = new XRWebGLBinding( session, gl );
|
||||
|
||||
this.lightProbe.addEventListener( 'reflectionchange', () => {
|
||||
|
||||
this.updateReflection();
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
// Start monitoring the XR animation frame loop to look for lighting
|
||||
// estimation changes.
|
||||
session.requestAnimationFrame( this.frameCallback );
|
||||
|
||||
}
|
||||
|
||||
updateReflection() {
|
||||
|
||||
const textureProperties = this.renderer.properties.get( this.xrLight.environment );
|
||||
|
||||
if ( textureProperties ) {
|
||||
|
||||
const cubeMap = this.xrWebGLBinding.getReflectionCubeMap( this.lightProbe );
|
||||
|
||||
if ( cubeMap ) {
|
||||
|
||||
textureProperties.__webglTexture = cubeMap;
|
||||
|
||||
this.xrLight.environment.needsPMREMUpdate = true;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
onXRFrame( time, xrFrame ) {
|
||||
|
||||
// If either this obejct or the XREstimatedLight has been destroyed, stop
|
||||
// running the frame loop.
|
||||
if ( ! this.xrLight ) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
const session = xrFrame.session;
|
||||
session.requestAnimationFrame( this.frameCallback );
|
||||
|
||||
const lightEstimate = xrFrame.getLightEstimate( this.lightProbe );
|
||||
if ( lightEstimate ) {
|
||||
|
||||
// We can copy the estimate's spherical harmonics array directly into the light probe.
|
||||
this.xrLight.lightProbe.sh.fromArray( lightEstimate.sphericalHarmonicsCoefficients );
|
||||
this.xrLight.lightProbe.intensity = 1.0;
|
||||
|
||||
// For the directional light we have to normalize the color and set the scalar as the
|
||||
// intensity, since WebXR can return color values that exceed 1.0.
|
||||
const intensityScalar = Math.max( 1.0,
|
||||
Math.max( lightEstimate.primaryLightIntensity.x,
|
||||
Math.max( lightEstimate.primaryLightIntensity.y,
|
||||
lightEstimate.primaryLightIntensity.z ) ) );
|
||||
|
||||
this.xrLight.directionalLight.color.setRGB(
|
||||
lightEstimate.primaryLightIntensity.x / intensityScalar,
|
||||
lightEstimate.primaryLightIntensity.y / intensityScalar,
|
||||
lightEstimate.primaryLightIntensity.z / intensityScalar );
|
||||
this.xrLight.directionalLight.intensity = intensityScalar;
|
||||
this.xrLight.directionalLight.position.copy( lightEstimate.primaryLightDirection );
|
||||
|
||||
if ( this.estimationStartCallback ) {
|
||||
|
||||
this.estimationStartCallback();
|
||||
this.estimationStartCallback = null;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dispose() {
|
||||
|
||||
this.xrLight = null;
|
||||
this.renderer = null;
|
||||
this.lightProbe = null;
|
||||
this.xrWebGLBinding = null;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class XREstimatedLight extends Group {
|
||||
|
||||
constructor( renderer, environmentEstimation = true ) {
|
||||
|
||||
super();
|
||||
|
||||
this.lightProbe = new LightProbe();
|
||||
this.lightProbe.intensity = 0;
|
||||
this.add( this.lightProbe );
|
||||
|
||||
this.directionalLight = new DirectionalLight();
|
||||
this.directionalLight.intensity = 0;
|
||||
this.add( this.directionalLight );
|
||||
|
||||
// Will be set to a cube map in the SessionLightProbe if environment estimation is
|
||||
// available and requested.
|
||||
this.environment = null;
|
||||
|
||||
let sessionLightProbe = null;
|
||||
let estimationStarted = false;
|
||||
renderer.xr.addEventListener( 'sessionstart', () => {
|
||||
|
||||
const session = renderer.xr.getSession();
|
||||
|
||||
if ( 'requestLightProbe' in session ) {
|
||||
|
||||
session.requestLightProbe( {
|
||||
|
||||
reflectionFormat: session.preferredReflectionFormat
|
||||
|
||||
} ).then( ( probe ) => {
|
||||
|
||||
sessionLightProbe = new SessionLightProbe( this, renderer, probe, environmentEstimation, () => {
|
||||
|
||||
estimationStarted = true;
|
||||
|
||||
// Fired to indicate that the estimated lighting values are now being updated.
|
||||
this.dispatchEvent( { type: 'estimationstart' } );
|
||||
|
||||
} );
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
} );
|
||||
|
||||
renderer.xr.addEventListener( 'sessionend', () => {
|
||||
|
||||
if ( sessionLightProbe ) {
|
||||
|
||||
sessionLightProbe.dispose();
|
||||
sessionLightProbe = null;
|
||||
|
||||
}
|
||||
|
||||
if ( estimationStarted ) {
|
||||
|
||||
// Fired to indicate that the estimated lighting values are no longer being updated.
|
||||
this.dispatchEvent( { type: 'estimationend' } );
|
||||
|
||||
}
|
||||
|
||||
} );
|
||||
|
||||
// Done inline to provide access to sessionLightProbe.
|
||||
this.dispose = () => {
|
||||
|
||||
if ( sessionLightProbe ) {
|
||||
|
||||
sessionLightProbe.dispose();
|
||||
sessionLightProbe = null;
|
||||
|
||||
}
|
||||
|
||||
this.remove( this.lightProbe );
|
||||
this.lightProbe = null;
|
||||
|
||||
this.remove( this.directionalLight );
|
||||
this.directionalLight = null;
|
||||
|
||||
this.environment = null;
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
}
|
114
dist/electron/static/sdk/three/jsm/webxr/XRHandMeshModel.js
vendored
Normal file
114
dist/electron/static/sdk/three/jsm/webxr/XRHandMeshModel.js
vendored
Normal file
@ -0,0 +1,114 @@
|
||||
import { GLTFLoader } from '../loaders/GLTFLoader.js';
|
||||
|
||||
const DEFAULT_HAND_PROFILE_PATH = 'https://cdn.jsdelivr.net/npm/@webxr-input-profiles/assets@1.0/dist/profiles/generic-hand/';
|
||||
|
||||
class XRHandMeshModel {
|
||||
|
||||
constructor( handModel, controller, path, handedness, loader = null, onLoad = null ) {
|
||||
|
||||
this.controller = controller;
|
||||
this.handModel = handModel;
|
||||
|
||||
this.bones = [];
|
||||
|
||||
if ( loader === null ) {
|
||||
|
||||
loader = new GLTFLoader();
|
||||
loader.setPath( path || DEFAULT_HAND_PROFILE_PATH );
|
||||
|
||||
}
|
||||
|
||||
loader.load( `${handedness}.glb`, gltf => {
|
||||
|
||||
const object = gltf.scene.children[ 0 ];
|
||||
this.handModel.add( object );
|
||||
|
||||
const mesh = object.getObjectByProperty( 'type', 'SkinnedMesh' );
|
||||
mesh.frustumCulled = false;
|
||||
mesh.castShadow = true;
|
||||
mesh.receiveShadow = true;
|
||||
|
||||
const joints = [
|
||||
'wrist',
|
||||
'thumb-metacarpal',
|
||||
'thumb-phalanx-proximal',
|
||||
'thumb-phalanx-distal',
|
||||
'thumb-tip',
|
||||
'index-finger-metacarpal',
|
||||
'index-finger-phalanx-proximal',
|
||||
'index-finger-phalanx-intermediate',
|
||||
'index-finger-phalanx-distal',
|
||||
'index-finger-tip',
|
||||
'middle-finger-metacarpal',
|
||||
'middle-finger-phalanx-proximal',
|
||||
'middle-finger-phalanx-intermediate',
|
||||
'middle-finger-phalanx-distal',
|
||||
'middle-finger-tip',
|
||||
'ring-finger-metacarpal',
|
||||
'ring-finger-phalanx-proximal',
|
||||
'ring-finger-phalanx-intermediate',
|
||||
'ring-finger-phalanx-distal',
|
||||
'ring-finger-tip',
|
||||
'pinky-finger-metacarpal',
|
||||
'pinky-finger-phalanx-proximal',
|
||||
'pinky-finger-phalanx-intermediate',
|
||||
'pinky-finger-phalanx-distal',
|
||||
'pinky-finger-tip',
|
||||
];
|
||||
|
||||
joints.forEach( jointName => {
|
||||
|
||||
const bone = object.getObjectByName( jointName );
|
||||
|
||||
if ( bone !== undefined ) {
|
||||
|
||||
bone.jointName = jointName;
|
||||
|
||||
} else {
|
||||
|
||||
console.warn( `Couldn't find ${jointName} in ${handedness} hand mesh` );
|
||||
|
||||
}
|
||||
|
||||
this.bones.push( bone );
|
||||
|
||||
} );
|
||||
|
||||
if ( onLoad ) onLoad( object );
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
updateMesh() {
|
||||
|
||||
// XR Joints
|
||||
const XRJoints = this.controller.joints;
|
||||
|
||||
for ( let i = 0; i < this.bones.length; i ++ ) {
|
||||
|
||||
const bone = this.bones[ i ];
|
||||
|
||||
if ( bone ) {
|
||||
|
||||
const XRJoint = XRJoints[ bone.jointName ];
|
||||
|
||||
if ( XRJoint.visible ) {
|
||||
|
||||
const position = XRJoint.position;
|
||||
|
||||
bone.position.copy( position );
|
||||
bone.quaternion.copy( XRJoint.quaternion );
|
||||
// bone.scale.setScalar( XRJoint.jointRadius || defaultRadius );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { XRHandMeshModel };
|
107
dist/electron/static/sdk/three/jsm/webxr/XRHandModelFactory.js
vendored
Normal file
107
dist/electron/static/sdk/three/jsm/webxr/XRHandModelFactory.js
vendored
Normal file
@ -0,0 +1,107 @@
|
||||
import {
|
||||
Object3D
|
||||
} from 'three';
|
||||
|
||||
import {
|
||||
XRHandPrimitiveModel
|
||||
} from './XRHandPrimitiveModel.js';
|
||||
|
||||
import {
|
||||
XRHandMeshModel
|
||||
} from './XRHandMeshModel.js';
|
||||
|
||||
class XRHandModel extends Object3D {
|
||||
|
||||
constructor( controller ) {
|
||||
|
||||
super();
|
||||
|
||||
this.controller = controller;
|
||||
this.motionController = null;
|
||||
this.envMap = null;
|
||||
|
||||
this.mesh = null;
|
||||
|
||||
}
|
||||
|
||||
updateMatrixWorld( force ) {
|
||||
|
||||
super.updateMatrixWorld( force );
|
||||
|
||||
if ( this.motionController ) {
|
||||
|
||||
this.motionController.updateMesh();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class XRHandModelFactory {
|
||||
|
||||
constructor( gltfLoader = null, onLoad = null ) {
|
||||
|
||||
this.gltfLoader = gltfLoader;
|
||||
this.path = null;
|
||||
this.onLoad = onLoad;
|
||||
|
||||
}
|
||||
|
||||
setPath( path ) {
|
||||
|
||||
this.path = path;
|
||||
|
||||
return this;
|
||||
|
||||
}
|
||||
|
||||
createHandModel( controller, profile ) {
|
||||
|
||||
const handModel = new XRHandModel( controller );
|
||||
|
||||
controller.addEventListener( 'connected', ( event ) => {
|
||||
|
||||
const xrInputSource = event.data;
|
||||
|
||||
if ( xrInputSource.hand && ! handModel.motionController ) {
|
||||
|
||||
handModel.xrInputSource = xrInputSource;
|
||||
|
||||
// @todo Detect profile if not provided
|
||||
if ( profile === undefined || profile === 'spheres' ) {
|
||||
|
||||
handModel.motionController = new XRHandPrimitiveModel( handModel, controller, this.path, xrInputSource.handedness, { primitive: 'sphere' } );
|
||||
|
||||
} else if ( profile === 'boxes' ) {
|
||||
|
||||
handModel.motionController = new XRHandPrimitiveModel( handModel, controller, this.path, xrInputSource.handedness, { primitive: 'box' } );
|
||||
|
||||
} else if ( profile === 'mesh' ) {
|
||||
|
||||
handModel.motionController = new XRHandMeshModel( handModel, controller, this.path, xrInputSource.handedness, this.gltfLoader, this.onLoad );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
controller.visible = true;
|
||||
|
||||
} );
|
||||
|
||||
controller.addEventListener( 'disconnected', () => {
|
||||
|
||||
controller.visible = false;
|
||||
// handModel.motionController = null;
|
||||
// handModel.remove( scene );
|
||||
// scene = null;
|
||||
|
||||
} );
|
||||
|
||||
return handModel;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { XRHandModelFactory };
|
103
dist/electron/static/sdk/three/jsm/webxr/XRHandPrimitiveModel.js
vendored
Normal file
103
dist/electron/static/sdk/three/jsm/webxr/XRHandPrimitiveModel.js
vendored
Normal file
@ -0,0 +1,103 @@
|
||||
import {
|
||||
DynamicDrawUsage,
|
||||
SphereGeometry,
|
||||
BoxGeometry,
|
||||
MeshStandardMaterial,
|
||||
InstancedMesh,
|
||||
Matrix4,
|
||||
Vector3
|
||||
} from 'three';
|
||||
|
||||
const _matrix = new Matrix4();
|
||||
const _vector = new Vector3();
|
||||
|
||||
class XRHandPrimitiveModel {
|
||||
|
||||
constructor( handModel, controller, path, handedness, options ) {
|
||||
|
||||
this.controller = controller;
|
||||
this.handModel = handModel;
|
||||
this.envMap = null;
|
||||
|
||||
let geometry;
|
||||
|
||||
if ( ! options || ! options.primitive || options.primitive === 'sphere' ) {
|
||||
|
||||
geometry = new SphereGeometry( 1, 10, 10 );
|
||||
|
||||
} else if ( options.primitive === 'box' ) {
|
||||
|
||||
geometry = new BoxGeometry( 1, 1, 1 );
|
||||
|
||||
}
|
||||
|
||||
const material = new MeshStandardMaterial();
|
||||
|
||||
this.handMesh = new InstancedMesh( geometry, material, 30 );
|
||||
this.handMesh.frustumCulled = false;
|
||||
this.handMesh.instanceMatrix.setUsage( DynamicDrawUsage ); // will be updated every frame
|
||||
this.handMesh.castShadow = true;
|
||||
this.handMesh.receiveShadow = true;
|
||||
this.handModel.add( this.handMesh );
|
||||
|
||||
this.joints = [
|
||||
'wrist',
|
||||
'thumb-metacarpal',
|
||||
'thumb-phalanx-proximal',
|
||||
'thumb-phalanx-distal',
|
||||
'thumb-tip',
|
||||
'index-finger-metacarpal',
|
||||
'index-finger-phalanx-proximal',
|
||||
'index-finger-phalanx-intermediate',
|
||||
'index-finger-phalanx-distal',
|
||||
'index-finger-tip',
|
||||
'middle-finger-metacarpal',
|
||||
'middle-finger-phalanx-proximal',
|
||||
'middle-finger-phalanx-intermediate',
|
||||
'middle-finger-phalanx-distal',
|
||||
'middle-finger-tip',
|
||||
'ring-finger-metacarpal',
|
||||
'ring-finger-phalanx-proximal',
|
||||
'ring-finger-phalanx-intermediate',
|
||||
'ring-finger-phalanx-distal',
|
||||
'ring-finger-tip',
|
||||
'pinky-finger-metacarpal',
|
||||
'pinky-finger-phalanx-proximal',
|
||||
'pinky-finger-phalanx-intermediate',
|
||||
'pinky-finger-phalanx-distal',
|
||||
'pinky-finger-tip'
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
updateMesh() {
|
||||
|
||||
const defaultRadius = 0.008;
|
||||
const joints = this.controller.joints;
|
||||
|
||||
let count = 0;
|
||||
|
||||
for ( let i = 0; i < this.joints.length; i ++ ) {
|
||||
|
||||
const joint = joints[ this.joints[ i ] ];
|
||||
|
||||
if ( joint.visible ) {
|
||||
|
||||
_vector.setScalar( joint.jointRadius || defaultRadius );
|
||||
_matrix.compose( joint.position, joint.quaternion, _vector );
|
||||
this.handMesh.setMatrixAt( i, _matrix );
|
||||
|
||||
count ++;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
this.handMesh.count = count;
|
||||
this.handMesh.instanceMatrix.needsUpdate = true;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { XRHandPrimitiveModel };
|
100
dist/electron/static/sdk/three/jsm/webxr/XRPlanes.js
vendored
Normal file
100
dist/electron/static/sdk/three/jsm/webxr/XRPlanes.js
vendored
Normal file
@ -0,0 +1,100 @@
|
||||
import {
|
||||
BoxGeometry,
|
||||
Matrix4,
|
||||
Mesh,
|
||||
MeshBasicMaterial,
|
||||
Object3D
|
||||
} from 'three';
|
||||
|
||||
class XRPlanes extends Object3D {
|
||||
|
||||
constructor( renderer ) {
|
||||
|
||||
super();
|
||||
|
||||
const matrix = new Matrix4();
|
||||
|
||||
const currentPlanes = new Map();
|
||||
|
||||
const xr = renderer.xr;
|
||||
|
||||
xr.addEventListener( 'planesdetected', event => {
|
||||
|
||||
const frame = event.data;
|
||||
const planes = frame.detectedPlanes;
|
||||
|
||||
const referenceSpace = xr.getReferenceSpace();
|
||||
|
||||
let planeschanged = false;
|
||||
|
||||
for ( const [ plane, mesh ] of currentPlanes ) {
|
||||
|
||||
if ( planes.has( plane ) === false ) {
|
||||
|
||||
mesh.geometry.dispose();
|
||||
mesh.material.dispose();
|
||||
this.remove( mesh );
|
||||
|
||||
currentPlanes.delete( plane );
|
||||
|
||||
planeschanged = true;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for ( const plane of planes ) {
|
||||
|
||||
if ( currentPlanes.has( plane ) === false ) {
|
||||
|
||||
const pose = frame.getPose( plane.planeSpace, referenceSpace );
|
||||
matrix.fromArray( pose.transform.matrix );
|
||||
|
||||
const polygon = plane.polygon;
|
||||
|
||||
let minX = Number.MAX_SAFE_INTEGER;
|
||||
let maxX = Number.MIN_SAFE_INTEGER;
|
||||
let minZ = Number.MAX_SAFE_INTEGER;
|
||||
let maxZ = Number.MIN_SAFE_INTEGER;
|
||||
|
||||
for ( const point of polygon ) {
|
||||
|
||||
minX = Math.min( minX, point.x );
|
||||
maxX = Math.max( maxX, point.x );
|
||||
minZ = Math.min( minZ, point.z );
|
||||
maxZ = Math.max( maxZ, point.z );
|
||||
|
||||
}
|
||||
|
||||
const width = maxX - minX;
|
||||
const height = maxZ - minZ;
|
||||
|
||||
const geometry = new BoxGeometry( width, 0.01, height );
|
||||
const material = new MeshBasicMaterial( { color: 0xffffff * Math.random() } );
|
||||
|
||||
const mesh = new Mesh( geometry, material );
|
||||
mesh.position.setFromMatrixPosition( matrix );
|
||||
mesh.quaternion.setFromRotationMatrix( matrix );
|
||||
this.add( mesh );
|
||||
|
||||
currentPlanes.set( plane, mesh );
|
||||
|
||||
planeschanged = true;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ( planeschanged ) {
|
||||
|
||||
this.dispatchEvent( { type: 'planeschanged' } );
|
||||
|
||||
}
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { XRPlanes };
|
Reference in New Issue
Block a user