|
|
@@ -9,6 +9,7 @@ const _lookDirection = new Vector3();
|
|
|
const _spherical = new Spherical();
|
|
|
const _target = new Vector3();
|
|
|
const _targetPosition = new Vector3();
|
|
|
+const _targetVelocity = new Vector3();
|
|
|
|
|
|
/**
|
|
|
* This class is an alternative implementation of {@link FlyControls}.
|
|
|
@@ -44,6 +45,15 @@ class FirstPersonControls extends Controls {
|
|
|
*/
|
|
|
this.lookSpeed = 0.005;
|
|
|
|
|
|
+ /**
|
|
|
+ * How quickly the movement and look velocity catches up to the input. Lower
|
|
|
+ * values feel heavier (more inertia), `1` disables damping.
|
|
|
+ *
|
|
|
+ * @type {number}
|
|
|
+ * @default 0.1
|
|
|
+ */
|
|
|
+ this.dampingFactor = 0.1;
|
|
|
+
|
|
|
/**
|
|
|
* Whether it's possible to vertically look around or not.
|
|
|
*
|
|
|
@@ -128,7 +138,7 @@ class FirstPersonControls extends Controls {
|
|
|
|
|
|
// internals
|
|
|
|
|
|
- this._autoSpeedFactor = 0.0;
|
|
|
+ this._velocity = new Vector3();
|
|
|
|
|
|
this._pointerX = 0;
|
|
|
this._pointerY = 0;
|
|
|
@@ -138,14 +148,23 @@ class FirstPersonControls extends Controls {
|
|
|
|
|
|
this._pointerCount = 0;
|
|
|
|
|
|
- this._moveForward = false;
|
|
|
- this._moveBackward = false;
|
|
|
+ // forward / backward come from keys and the pointer, tracked per source so they don't
|
|
|
+ // clobber: while a forward / backward key is held, a click only looks
|
|
|
+ this._keyForward = false;
|
|
|
+ this._keyBackward = false;
|
|
|
+ this._pointerForward = false;
|
|
|
+ this._pointerBackward = false;
|
|
|
this._moveLeft = false;
|
|
|
this._moveRight = false;
|
|
|
+ this._moveUp = false;
|
|
|
+ this._moveDown = false;
|
|
|
|
|
|
this._lat = 0;
|
|
|
this._lon = 0;
|
|
|
|
|
|
+ this._lonVelocity = 0;
|
|
|
+ this._latVelocity = 0;
|
|
|
+
|
|
|
// event listeners
|
|
|
|
|
|
this._onPointerMove = onPointerMove.bind( this );
|
|
|
@@ -174,11 +193,15 @@ class FirstPersonControls extends Controls {
|
|
|
window.addEventListener( 'keydown', this._onKeyDown );
|
|
|
window.addEventListener( 'keyup', this._onKeyUp );
|
|
|
|
|
|
- this.domElement.addEventListener( 'pointermove', this._onPointerMove );
|
|
|
this.domElement.addEventListener( 'pointerdown', this._onPointerDown );
|
|
|
- this.domElement.addEventListener( 'pointerup', this._onPointerUp );
|
|
|
this.domElement.addEventListener( 'contextmenu', this._onContextMenu );
|
|
|
|
|
|
+ const { ownerDocument } = this.domElement;
|
|
|
+
|
|
|
+ ownerDocument.addEventListener( 'pointermove', this._onPointerMove );
|
|
|
+ ownerDocument.addEventListener( 'pointerup', this._onPointerUp );
|
|
|
+ ownerDocument.addEventListener( 'pointercancel', this._onPointerUp );
|
|
|
+
|
|
|
this.domElement.style.touchAction = 'none'; // Disable touch scroll
|
|
|
|
|
|
}
|
|
|
@@ -188,11 +211,15 @@ class FirstPersonControls extends Controls {
|
|
|
window.removeEventListener( 'keydown', this._onKeyDown );
|
|
|
window.removeEventListener( 'keyup', this._onKeyUp );
|
|
|
|
|
|
- this.domElement.removeEventListener( 'pointermove', this._onPointerMove );
|
|
|
this.domElement.removeEventListener( 'pointerdown', this._onPointerDown );
|
|
|
- this.domElement.removeEventListener( 'pointerup', this._onPointerUp );
|
|
|
this.domElement.removeEventListener( 'contextmenu', this._onContextMenu );
|
|
|
|
|
|
+ const { ownerDocument } = this.domElement;
|
|
|
+
|
|
|
+ ownerDocument.removeEventListener( 'pointermove', this._onPointerMove );
|
|
|
+ ownerDocument.removeEventListener( 'pointerup', this._onPointerUp );
|
|
|
+ ownerDocument.removeEventListener( 'pointercancel', this._onPointerUp );
|
|
|
+
|
|
|
this.domElement.style.touchAction = ''; // Restore touch scroll
|
|
|
|
|
|
}
|
|
|
@@ -235,31 +262,35 @@ class FirstPersonControls extends Controls {
|
|
|
|
|
|
if ( this.enabled === false ) return;
|
|
|
|
|
|
- if ( this.heightSpeed ) {
|
|
|
+ const moveForward = this._keyForward || this._pointerForward;
|
|
|
+ const moveBackward = this._keyBackward || this._pointerBackward;
|
|
|
|
|
|
- const y = MathUtils.clamp( this.object.position.y, this.heightMin, this.heightMax );
|
|
|
- const heightDelta = y - this.heightMin;
|
|
|
+ const forward = moveForward || ( this.autoForward && ! moveBackward );
|
|
|
|
|
|
- this._autoSpeedFactor = delta * ( heightDelta * this.heightCoef );
|
|
|
+ // target velocity in the object's local space
|
|
|
|
|
|
- } else {
|
|
|
+ _targetVelocity.set(
|
|
|
+ ( this._moveRight ? 1 : 0 ) - ( this._moveLeft ? 1 : 0 ),
|
|
|
+ ( this._moveUp ? 1 : 0 ) - ( this._moveDown ? 1 : 0 ),
|
|
|
+ ( moveBackward ? 1 : 0 ) - ( forward ? 1 : 0 )
|
|
|
+ ).multiplyScalar( this.movementSpeed );
|
|
|
|
|
|
- this._autoSpeedFactor = 0.0;
|
|
|
+ // faster forward movement the higher the camera is
|
|
|
|
|
|
- }
|
|
|
+ if ( forward && this.heightSpeed ) {
|
|
|
|
|
|
- const actualMoveSpeed = delta * this.movementSpeed;
|
|
|
+ const y = MathUtils.clamp( this.object.position.y, this.heightMin, this.heightMax );
|
|
|
+ _targetVelocity.z -= ( y - this.heightMin ) * this.heightCoef;
|
|
|
|
|
|
- if ( this._moveForward || ( this.autoForward && ! this._moveBackward ) ) this.object.translateZ( - ( actualMoveSpeed + this._autoSpeedFactor ) );
|
|
|
- if ( this._moveBackward ) this.object.translateZ( actualMoveSpeed );
|
|
|
+ }
|
|
|
|
|
|
- if ( this._moveLeft ) this.object.translateX( - actualMoveSpeed );
|
|
|
- if ( this._moveRight ) this.object.translateX( actualMoveSpeed );
|
|
|
+ // ease toward the target velocity for smooth acceleration and deceleration
|
|
|
|
|
|
- if ( this._moveUp ) this.object.translateY( actualMoveSpeed );
|
|
|
- if ( this._moveDown ) this.object.translateY( - actualMoveSpeed );
|
|
|
+ this._velocity.lerp( _targetVelocity, this.dampingFactor );
|
|
|
|
|
|
- const actualLookSpeed = delta * this.lookSpeed;
|
|
|
+ this.object.translateX( this._velocity.x * delta );
|
|
|
+ this.object.translateY( this._velocity.y * delta );
|
|
|
+ this.object.translateZ( this._velocity.z * delta );
|
|
|
|
|
|
let verticalLookRatio = 1;
|
|
|
|
|
|
@@ -269,12 +300,16 @@ class FirstPersonControls extends Controls {
|
|
|
|
|
|
}
|
|
|
|
|
|
- if ( this.mouseDragOn ) {
|
|
|
+ // target look velocity, zero when not dragging so the view eases to a stop
|
|
|
|
|
|
- this._lon -= this._pointerX * actualLookSpeed;
|
|
|
- if ( this.lookVertical ) this._lat -= this._pointerY * actualLookSpeed * verticalLookRatio;
|
|
|
+ const targetLon = this.mouseDragOn ? - this._pointerX * this.lookSpeed : 0;
|
|
|
+ const targetLat = ( this.mouseDragOn && this.lookVertical ) ? - this._pointerY * this.lookSpeed * verticalLookRatio : 0;
|
|
|
|
|
|
- }
|
|
|
+ this._lonVelocity = MathUtils.lerp( this._lonVelocity, targetLon, this.dampingFactor );
|
|
|
+ this._latVelocity = MathUtils.lerp( this._latVelocity, targetLat, this.dampingFactor );
|
|
|
+
|
|
|
+ this._lon += this._lonVelocity * delta;
|
|
|
+ this._lat += this._latVelocity * delta;
|
|
|
|
|
|
this._lat = Math.max( - 85, Math.min( 85, this._lat ) );
|
|
|
|
|
|
@@ -332,15 +367,15 @@ function onPointerDown( event ) {
|
|
|
|
|
|
if ( event.pointerType === 'touch' ) {
|
|
|
|
|
|
- this._moveForward = this._pointerCount === 1;
|
|
|
- this._moveBackward = this._pointerCount >= 2;
|
|
|
+ this._pointerForward = this._pointerCount === 1;
|
|
|
+ this._pointerBackward = this._pointerCount >= 2;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
switch ( event.button ) {
|
|
|
|
|
|
- case 0: this._moveForward = true; break;
|
|
|
- case 2: this._moveBackward = true; break;
|
|
|
+ case 0: if ( ! this._keyForward && ! this._keyBackward ) this._pointerForward = true; break;
|
|
|
+ case 2: if ( ! this._keyForward && ! this._keyBackward ) this._pointerBackward = true; break;
|
|
|
|
|
|
}
|
|
|
|
|
|
@@ -358,21 +393,23 @@ function onPointerDown( event ) {
|
|
|
|
|
|
function onPointerUp( event ) {
|
|
|
|
|
|
+ if ( this.mouseDragOn === false ) return;
|
|
|
+
|
|
|
this.domElement.releasePointerCapture( event.pointerId );
|
|
|
|
|
|
this._pointerCount --;
|
|
|
|
|
|
if ( event.pointerType === 'touch' ) {
|
|
|
|
|
|
- this._moveForward = this._pointerCount === 1;
|
|
|
- this._moveBackward = false;
|
|
|
+ this._pointerForward = this._pointerCount === 1;
|
|
|
+ this._pointerBackward = false;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
switch ( event.button ) {
|
|
|
|
|
|
- case 0: this._moveForward = false; break;
|
|
|
- case 2: this._moveBackward = false; break;
|
|
|
+ case 0: this._pointerForward = false; break;
|
|
|
+ case 2: this._pointerBackward = false; break;
|
|
|
|
|
|
}
|
|
|
|
|
|
@@ -399,19 +436,19 @@ function onKeyDown( event ) {
|
|
|
switch ( event.code ) {
|
|
|
|
|
|
case 'ArrowUp':
|
|
|
- case 'KeyW': this._moveForward = true; break;
|
|
|
+ case 'KeyW': this._keyForward = true; break;
|
|
|
|
|
|
case 'ArrowLeft':
|
|
|
case 'KeyA': this._moveLeft = true; break;
|
|
|
|
|
|
case 'ArrowDown':
|
|
|
- case 'KeyS': this._moveBackward = true; break;
|
|
|
+ case 'KeyS': this._keyBackward = true; break;
|
|
|
|
|
|
case 'ArrowRight':
|
|
|
case 'KeyD': this._moveRight = true; break;
|
|
|
|
|
|
- case 'KeyR': this._moveUp = true; break;
|
|
|
- case 'KeyF': this._moveDown = true; break;
|
|
|
+ case 'KeyE': this._moveUp = true; break;
|
|
|
+ case 'KeyQ': this._moveDown = true; break;
|
|
|
|
|
|
}
|
|
|
|
|
|
@@ -422,19 +459,19 @@ function onKeyUp( event ) {
|
|
|
switch ( event.code ) {
|
|
|
|
|
|
case 'ArrowUp':
|
|
|
- case 'KeyW': this._moveForward = false; break;
|
|
|
+ case 'KeyW': this._keyForward = false; break;
|
|
|
|
|
|
case 'ArrowLeft':
|
|
|
case 'KeyA': this._moveLeft = false; break;
|
|
|
|
|
|
case 'ArrowDown':
|
|
|
- case 'KeyS': this._moveBackward = false; break;
|
|
|
+ case 'KeyS': this._keyBackward = false; break;
|
|
|
|
|
|
case 'ArrowRight':
|
|
|
case 'KeyD': this._moveRight = false; break;
|
|
|
|
|
|
- case 'KeyR': this._moveUp = false; break;
|
|
|
- case 'KeyF': this._moveDown = false; break;
|
|
|
+ case 'KeyE': this._moveUp = false; break;
|
|
|
+ case 'KeyQ': this._moveDown = false; break;
|
|
|
|
|
|
}
|
|
|
|