UNPKG

three

Version:

JavaScript 3D library

540 lines (410 loc) 13.5 kB
import { BufferGeometry, Float32BufferAttribute, Matrix4, Mesh, MeshBasicMaterial, Object3D, Raycaster, SphereGeometry, Vector3 } 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 Vector3( 0, 1, 0 ); const ZAXIS = /* @__PURE__ */ new Vector3( 0, 0, 1 ); const CURSOR_RADIUS = 0.02; const CURSOR_MAX_DISTANCE = 1.5; /** * Represents an Oculus hand pointer model. * * @augments Object3D * @three_import import { OculusHandPointerModel } from 'three/addons/webxr/OculusHandPointerModel.js'; */ class OculusHandPointerModel extends Object3D { /** * Constructs a new Oculus hand model. * * @param {Group} hand - The hand controller. * @param {Group} controller - The WebXR controller in target ray space. */ constructor( hand, controller ) { super(); /** * The hand controller. * * @type {Group} */ this.hand = hand; /** * The WebXR controller in target ray space. * * @type {Group} */ this.controller = controller; // Unused this.motionController = null; this.envMap = null; this.mesh = null; /** * The pointer geometry. * * @type {?BufferGeometry} * @default null */ this.pointerGeometry = null; /** * The pointer mesh. * * @type {?Mesh} * @default null */ this.pointerMesh = null; /** * The pointer object that holds the pointer mesh. * * @type {?Object3D} * @default null */ this.pointerObject = null; /** * Whether the model is pinched or not. * * @type {?boolean} * @default false */ this.pinched = false; /** * Whether the model is attached or not. * * @type {boolean} * @default false */ this.attached = false; /** * The cursor object. * * @type {?Mesh} * @default null */ this.cursorObject = null; /** * The internal raycaster used for detecting * intersections. * * @type {?Raycaster} * @default 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 Vector3( POINTER_FRONT_RADIUS, 0, - 1 * ( POINTER_LENGTH - rearRadius ) ); this._drawVerticesRing( vertices, frontFaceBase, 0 ); // rings for rear hemisphere const rearBase = new 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 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 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 Float32BufferAttribute( vertices, 3 ) ); // verticesNeedUpdate = true; } /** * Creates a pointer mesh and adds it to this model. */ createPointer() { let i, j; const vertices = new Array( ( ( POINTER_RINGS + 1 ) * POINTER_SEGMENTS + 2 ) * 3 ).fill( 0 ); // const vertices = []; const indices = []; this.pointerGeometry = new BufferGeometry(); this.pointerGeometry.setAttribute( 'position', new 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 MeshBasicMaterial(); material.transparent = true; material.opacity = POINTER_OPACITY_MIN; this.pointerGeometry.setIndex( indices ); this.pointerMesh = new Mesh( this.pointerGeometry, material ); this.pointerMesh.position.set( 0, 0, - 1 * POINTER_REAR_RADIUS ); this.pointerObject = new Object3D(); this.pointerObject.add( this.pointerMesh ); this.raycaster = new Raycaster(); // create cursor const cursorGeometry = new SphereGeometry( CURSOR_RADIUS, 10, 10 ); const cursorMaterial = new MeshBasicMaterial(); cursorMaterial.transparent = true; cursorMaterial.opacity = POINTER_OPACITY_MIN; this.cursorObject = new Mesh( cursorGeometry, cursorMaterial ); this.pointerObject.add( this.cursorObject ); this.add( this.pointerObject ); } _updateRaycaster() { if ( this.raycaster ) { const pointerMatrix = this.pointerObject.matrixWorld; const tempMatrix = new 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; } /** * Overwritten with a custom implementation. Makes sure the internal pointer and raycaster are updated. * * @param {boolean} [force=false] - When set to `true`, a recomputation of world matrices is forced even * when {@link Object3D#matrixWorldAutoUpdate} is set to `false`. */ updateMatrixWorld( force ) { super.updateMatrixWorld( force ); if ( this.pointerGeometry ) { this._updatePointer(); this._updateRaycaster(); } } /** * Returns `true` is the model is pinched. * * @return {boolean} Whether the model is pinched or not. */ isPinched() { return this.pinched; } /** * Sets the attached state. * * @param {boolean} attached - Whether the model is attached or not. */ setAttached( attached ) { this.attached = attached; } /** * Returns `true` is the model is attached. * * @return {boolean} Whether the model is attached or not. */ isAttached() { return this.attached; } /** * Performs an intersection test with the model's raycaster and the given object. * * @param {Object3D} object - The 3D object to check for intersection with the ray. * @param {boolean} [recursive=true] - If set to `true`, it also checks all descendants. * Otherwise it only checks intersection with the object. * @return {Array<Raycaster~Intersection>} An array holding the intersection points. */ intersectObject( object, recursive = true ) { if ( this.raycaster ) { return this.raycaster.intersectObject( object, recursive ); } } /** * Performs an intersection test with the model's raycaster and the given objects. * * @param {Array<Object3D>} objects - The 3D objects to check for intersection with the ray. * @param {boolean} [recursive=true] - If set to `true`, it also checks all descendants. * Otherwise it only checks intersection with the object. * @return {Array<Raycaster~Intersection>} An array holding the intersection points. */ intersectObjects( objects, recursive = true ) { if ( this.raycaster ) { return this.raycaster.intersectObjects( objects, recursive ); } } /** * Checks for intersections between the model's raycaster and the given objects. The method * updates the cursor object to the intersection point. * * @param {Array<Object3D>} objects - The 3D objects to check for intersection with the ray. * @param {boolean} [recursive=false] - If set to `true`, it also checks all descendants. * Otherwise it only checks intersection with the object. */ checkIntersections( objects, recursive = false ) { if ( this.raycaster && ! this.attached ) { const intersections = this.raycaster.intersectObjects( objects, recursive ); const direction = new 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 ) ); } } } /** * Sets the cursor to the given distance. * * @param {number} distance - The distance to set the cursor to. */ setCursor( distance ) { const direction = new Vector3( 0, 0, - 1 ); if ( this.raycaster && ! this.attached ) { this.cursorObject.position.copy( direction.multiplyScalar( distance ) ); } } /** * Frees the GPU-related resources allocated by this instance. Call this * method whenever this instance is no longer used in your app. */ dispose() { this._onDisconnected(); this.hand.removeEventListener( 'connected', this._onConnected ); this.hand.removeEventListener( 'disconnected', this._onDisconnected ); } } export { OculusHandPointerModel };