Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ See [the demo](https://github.com/yomotsu/camera-movement-comparison#dolly-vs-zo
| `.colliderMeshes` | `array` | `[]` | An array of Meshes to collide with camera ². |
| `.infinityDolly` | `boolean` | `false` | `true` to enable Infinity Dolly for wheel and pinch. Use this with `minDistance` and `maxDistance` ³. |
| `.restThreshold` | `number` | `0.0025` | Controls how soon the `rest` event fires as the camera slows |
| `.smoothRotationTheta` | `boolean` | `true` | `true` to smooth azimuth (`theta`) transitions along the shortest path (wraps angles such as -179° ↔ 179°). |

1. Every 360 degrees turn is added to `.azimuthAngle` value, which is accumulative.
`360º = 360 * THREE.MathUtils.DEG2RAD = Math.PI * 2`, `720º = Math.PI * 4`.
Expand Down Expand Up @@ -659,6 +660,8 @@ Set current camera position as the default position
Normalize camera azimuth angle (horizontal rotation) between -180 and 180 degrees.
This is useful when you want to keep the azimuth angle normalized before calling methods like `.setLookAt()`, `.lerpLookAt()`, `.setTarget()`, `.setPosition()`, and `.reset()`.

When combined with the `.smoothRotationTheta` property (enabled by default), theta transitions always follow the shortest rotational path during smoothing.

This method returns the CameraControls instance itself, so you can chain it with other methods.

#### `reset( enableTransition )`
Expand Down
9 changes: 8 additions & 1 deletion src/CameraControls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,13 @@ export class CameraControls extends EventDispatcher {
*/
restThreshold = 0.01;

/**
* `true` to enable smooth rotation interpolation along the shortest path for azimuth (theta).
* When enabled, rotating between angles like -179° and 179° will take the short 2° path instead of rotating 358°.
* @category Properties
*/
smoothRotationTheta = true;

/**
* An array of Meshes to collide with camera.
* Be aware colliderMeshes may decrease performance. The collision test uses 4 raycasters from the camera since the near plane has 4 corners.
Expand Down Expand Up @@ -2619,7 +2626,7 @@ export class CameraControls extends EventDispatcher {
} else {

const smoothTime = this._isUserControllingRotate ? this.draggingSmoothTime : this.smoothTime;
this._spherical.theta = smoothDamp( this._spherical.theta, this._sphericalEnd.theta, this._thetaVelocity, smoothTime, Infinity, delta );
this._spherical.theta = smoothDamp( this._spherical.theta, this._sphericalEnd.theta, this._thetaVelocity, smoothTime, Infinity, delta, this.smoothRotationTheta ? 'shortestAngle' : 'none' );
this._needsUpdate = true;

}
Expand Down
29 changes: 25 additions & 4 deletions src/utils/math-utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import type * as _THREE from 'three';
import { MathUtils } from 'three';
import type { Ref } from '../types';

const EPSILON = 1e-5;
export const DEG2RAD = Math.PI / 180;
export const TAU = Math.PI * 2;

export type SmoothDampWrapMode = 'none' | 'shortestAngle';

export function absoluteAngle( targetAngle: number, sourceAngle: number ): number {
const angle = targetAngle - sourceAngle
return MathUtils.euclideanModulo( angle + Math.PI, TAU ) - Math.PI;
}


export function clamp( value: number, min: number, max: number ) {

Expand Down Expand Up @@ -55,6 +65,7 @@ export function smoothDamp(
smoothTime: number,
maxSpeed: number = Infinity,
deltaTime: number,
smoothMode?: SmoothDampWrapMode,
): number {

// Based on Game Programming Gems 4 Chapter 1.10
Expand All @@ -63,17 +74,27 @@ export function smoothDamp(

const x = omega * deltaTime;
const exp = 1 / ( 1 + x + 0.48 * x * x + 0.235 * x * x * x );
let change = current - target;
const originalTo = target;

let targetPosition = target;

if ( smoothMode === 'shortestAngle' ) {

const delta = absoluteAngle( target, current );
targetPosition = current + delta;

}

let change = current - targetPosition;
const originalTo = targetPosition;

// Clamp maximum speed
const maxChange = maxSpeed * smoothTime;
change = clamp( change, - maxChange, maxChange );
target = current - change;
targetPosition = current - change;

const temp = ( currentVelocityRef.value + omega * change ) * deltaTime;
currentVelocityRef.value = ( currentVelocityRef.value - omega * temp ) * exp;
let output = target + ( change + temp ) * exp;
let output = targetPosition + ( change + temp ) * exp;

// Prevent overshooting
if ( originalTo - current > 0.0 === output > originalTo ) {
Expand Down