Skip to content

Commit c86c8f2

Browse files
committed
feat: wrap around transitions between azimuth angles close to ±180°
1 parent 2086f05 commit c86c8f2

File tree

3 files changed

+36
-5
lines changed

3 files changed

+36
-5
lines changed

readme.md

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

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

663+
When combined with the `.smoothRotationTheta` property (enabled by default), theta transitions always follow the shortest rotational path during smoothing.
664+
662665
This method returns the CameraControls instance itself, so you can chain it with other methods.
663666

664667
#### `reset( enableTransition )`

src/CameraControls.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,13 @@ export class CameraControls extends EventDispatcher {
296296
*/
297297
restThreshold = 0.01;
298298

299+
/**
300+
* `true` to enable smooth rotation interpolation along the shortest path for azimuth (theta).
301+
* When enabled, rotating between angles like -179° and 179° will take the short 2° path instead of rotating 358°.
302+
* @category Properties
303+
*/
304+
smoothRotationTheta = true;
305+
299306
/**
300307
* An array of Meshes to collide with camera.
301308
* Be aware colliderMeshes may decrease performance. The collision test uses 4 raycasters from the camera since the near plane has 4 corners.
@@ -2619,7 +2626,7 @@ export class CameraControls extends EventDispatcher {
26192626
} else {
26202627

26212628
const smoothTime = this._isUserControllingRotate ? this.draggingSmoothTime : this.smoothTime;
2622-
this._spherical.theta = smoothDamp( this._spherical.theta, this._sphericalEnd.theta, this._thetaVelocity, smoothTime, Infinity, delta );
2629+
this._spherical.theta = smoothDamp( this._spherical.theta, this._sphericalEnd.theta, this._thetaVelocity, smoothTime, Infinity, delta, this.smoothRotationTheta ? 'shortestAngle' : 'none' );
26232630
this._needsUpdate = true;
26242631

26252632
}

src/utils/math-utils.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
11
import type * as _THREE from 'three';
2+
import { MathUtils } from 'three';
23
import type { Ref } from '../types';
34

45
const EPSILON = 1e-5;
56
export const DEG2RAD = Math.PI / 180;
7+
export const TAU = Math.PI * 2;
8+
9+
export type SmoothDampWrapMode = 'none' | 'shortestAngle';
10+
11+
export function absoluteAngle( targetAngle: number, sourceAngle: number ): number {
12+
const angle = targetAngle - sourceAngle
13+
return MathUtils.euclideanModulo( angle + Math.PI, TAU ) - Math.PI;
14+
}
15+
616

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

@@ -55,6 +65,7 @@ export function smoothDamp(
5565
smoothTime: number,
5666
maxSpeed: number = Infinity,
5767
deltaTime: number,
68+
smoothMode?: SmoothDampWrapMode,
5869
): number {
5970

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

6475
const x = omega * deltaTime;
6576
const exp = 1 / ( 1 + x + 0.48 * x * x + 0.235 * x * x * x );
66-
let change = current - target;
67-
const originalTo = target;
77+
78+
let targetPosition = target;
79+
80+
if ( smoothMode === 'shortestAngle' ) {
81+
82+
const delta = absoluteAngle( target, current );
83+
targetPosition = current + delta;
84+
85+
}
86+
87+
let change = current - targetPosition;
88+
const originalTo = targetPosition;
6889

6990
// Clamp maximum speed
7091
const maxChange = maxSpeed * smoothTime;
7192
change = clamp( change, - maxChange, maxChange );
72-
target = current - change;
93+
targetPosition = current - change;
7394

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

7899
// Prevent overshooting
79100
if ( originalTo - current > 0.0 === output > originalTo ) {

0 commit comments

Comments
 (0)