Skip to content

Commit

Permalink
added elasticity to orbit camera controller and free camera rotation
Browse files Browse the repository at this point in the history
  • Loading branch information
AndyGura committed Nov 14, 2024
1 parent bb7974f commit 71d653d
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 31 deletions.
2 changes: 1 addition & 1 deletion examples/shooter-three-ammo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ world.init().then(async () => {
mouseOptions: { canvas, pointerLock: true },
cameraLinearSpeed: 50,
cameraMovementElasticity: 100,
cameraRotationMultiplier: 0.8,
cameraRotationSensitivity: 0.8,
ignoreMouseUnlessPointerLocked: true,
ignoreKeyboardUnlessPointerLocked: true,
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { combineLatest, filter, takeUntil } from 'rxjs';
import { BehaviorSubject, combineLatest, filter, takeUntil } from 'rxjs';
import {
DirectionKeyboardInput,
DirectionKeyboardKeymap,
DirectionKeyboardOutput,
ggElastic,
GgWorld,
IEntity,
KeyboardInput,
Expand All @@ -11,14 +12,13 @@ import {
MutableSpherical,
Pnt2,
Pnt3,
Point2,
Point3,
Qtrn,
Spherical,
TickOrder,
} from '../../../../base';
import { Renderer3dEntity } from '../../renderer-3d.entity';
import { map } from 'rxjs/operators';
import ggElastic from '../../../../base/pipes/gg-elastic.pipe';

/**
* Options for configuring a FreeCameraInput controller.
Expand All @@ -43,7 +43,11 @@ export type FreeCameraControllerOptions = {
/**
* The speed of camera rotation in radians per 1000px mouse movement. 1 by default
*/
cameraRotationMultiplier: number;
cameraRotationSensitivity: number;
/**
* An elasticity factor for camera rotation. 0 by default (no elastic motion)
*/
cameraRotationElasticity: number;
/**
* Flag to ignore cursor movement if pointer was not locked. false by default
*/
Expand All @@ -63,7 +67,8 @@ const DEFAULT_FREE_CAMERA_CONTROLLER_OPTIONS: FreeCameraControllerOptions = {
cameraLinearSpeed: 20,
cameraMovementElasticity: 0,
cameraBoostMultiplier: 2.5,
cameraRotationMultiplier: 1,
cameraRotationSensitivity: 1,
cameraRotationElasticity: 0,
mouseOptions: {},
ignoreMouseUnlessPointerLocked: false,
ignoreKeyboardUnlessPointerLocked: false,
Expand Down Expand Up @@ -161,16 +166,44 @@ export class FreeCameraController extends IEntity {
});

// Subscribe to mouse input for camera rotation
let rotationDelta: Point2 = Pnt2.O;
const spherical: MutableSpherical = Pnt3.toSpherical(Pnt3.rot({ x: 0, y: 0, z: -1 }, this.camera.rotation));
let isTouchScreen = MouseInput.isTouchDevice();
this.mouseInput.delta$
.pipe(
let mouseDelta$ = this.mouseInput.delta$.pipe(
takeUntil(this._onRemoved$),
filter(() => isTouchScreen || !this.options.ignoreMouseUnlessPointerLocked || this.mouseInput.isPointerLocked),
);
if (this.options.cameraRotationElasticity > 0) {
const s$: BehaviorSubject<Spherical> = new BehaviorSubject(spherical);
mouseDelta$.subscribe(delta => {
const s = s$.getValue();
s$.next({
phi: Math.max(
0.000001,
Math.min(Math.PI - 0.000001, s.phi + (delta.y * this.options.cameraRotationSensitivity) / 1000),
),
theta: s.theta - (delta.x * this.options.cameraRotationSensitivity) / 1000,
radius: 1,
});
});
s$.pipe(
takeUntil(this._onRemoved$),
filter(() => isTouchScreen || !this.options.ignoreMouseUnlessPointerLocked || this.mouseInput.isPointerLocked),
)
.subscribe(delta => {
rotationDelta = Pnt2.add(rotationDelta, delta);
ggElastic(
this.tick$,
this.options.cameraRotationElasticity,
(a, b, f) => ({ phi: a.phi + f * (b.phi - a.phi), theta: a.theta + f * (b.theta - a.theta), radius: 1 }),
(a, b) => Pnt2.dist({ x: a.phi, y: a.theta }, { x: b.phi, y: b.theta }) < 0.0001,
),
).subscribe(s => {
spherical.theta = s.theta;
spherical.phi = s.phi;
});
} else {
mouseDelta$.subscribe(delta => {
spherical.theta -= (delta.x * this.options.cameraRotationSensitivity) / 1000;
spherical.phi += (delta.y * this.options.cameraRotationSensitivity) / 1000;
spherical.phi = Math.max(0.000001, Math.min(Math.PI - 0.000001, spherical.phi));
});
}

// Setup updating camera position and rotation based on input
this.camera.tick$.pipe(takeUntil(this._onRemoved$)).subscribe(([_, delta]) => {
Expand All @@ -182,17 +215,10 @@ export class FreeCameraController extends IEntity {
this.camera.rotation,
),
);
if (rotationDelta.x != 0 || rotationDelta.y != 0) {
const spherical: MutableSpherical = Pnt3.toSpherical(Pnt3.rot({ x: 0, y: 0, z: -1 }, this.camera.rotation));
spherical.theta -= (rotationDelta.x * this.options.cameraRotationMultiplier) / 1000;
spherical.phi += (rotationDelta.y * this.options.cameraRotationMultiplier) / 1000;
spherical.phi = Math.max(0.000001, Math.min(Math.PI - 0.000001, spherical.phi));
this.camera.rotation = Qtrn.lookAt(
this.camera.position,
Pnt3.add(this.camera.position, Pnt3.fromSpherical(spherical)),
);
rotationDelta = Pnt2.O;
}
this.camera.rotation = Qtrn.lookAt(
this.camera.position,
Pnt3.add(this.camera.position, Pnt3.fromSpherical(spherical)),
);
});

// start input
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import {
ggElastic,
GgWorld,
IEntity,
MouseInput,
MouseInputOptions,
MouseInputState,
MutableSpherical,
Pnt2,
Pnt3,
Point2,
Point3,
Qtrn,
Spherical,
TickOrder,
} from '../../../../base';
import { filter, takeUntil } from 'rxjs';
import { BehaviorSubject, filter, takeUntil } from 'rxjs';
import { map } from 'rxjs/operators';
import { Renderer3dEntity } from '../../renderer-3d.entity';

Expand All @@ -24,6 +27,10 @@ export type OrbitCameraControllerOptions = {
* Orbiting options. false disables orbiting, sensitivity fields are the speed in radians per 1000px mouse movement. 1 by default
*/
orbiting: { sensitivityX: number; sensitivityY: number } | false;
/**
* An elasticity factor for orbiting. 0 by default (no elastic motion)
*/
orbitingElasticity: number;
/**
* Zooming options. false disables zooming. Enabled by default
*/
Expand All @@ -41,6 +48,7 @@ export type OrbitCameraControllerOptions = {
const DEFAULT_OPTIONS: OrbitCameraControllerOptions = {
mouseOptions: {},
orbiting: { sensitivityX: 1, sensitivityY: 1 },
orbitingElasticity: 0,
zooming: { sensitivity: 1 },
panning: { sensitivityX: 1, sensitivityY: 1 },
dollying: { sensitivity: 1 },
Expand Down Expand Up @@ -87,16 +95,42 @@ export class OrbitCameraController extends IEntity {
await super.onSpawned(world);
this.spherical = Pnt3.toSpherical(Pnt3.sub(this.camera.position, this.target));
if (this.options.orbiting) {
this.mouseInput.delta$
.pipe(
let mouseDelta$ = this.mouseInput.delta$.pipe(
takeUntil(this._onRemoved$),
filter(() => this.mouseInput.state == MouseInputState.DRAG),
);
if (this.options.orbitingElasticity > 0) {
const s$: BehaviorSubject<Spherical> = new BehaviorSubject(this.spherical);
mouseDelta$.subscribe(delta => {
const s = s$.getValue();
s$.next({
phi: Math.max(
0.000001,
Math.min(Math.PI - 0.000001, s.phi - (delta.y * (this.options.orbiting as any).sensitivityY) / 1000),
),
theta: s.theta - (delta.x * (this.options.orbiting as any).sensitivityX) / 1000,
radius: 1,
});
});
s$.pipe(
takeUntil(this._onRemoved$),
filter(() => this.mouseInput.state == MouseInputState.DRAG),
)
.subscribe(delta => {
ggElastic(
this.tick$,
this.options.orbitingElasticity,
(a, b, f) => ({ phi: a.phi + f * (b.phi - a.phi), theta: a.theta + f * (b.theta - a.theta), radius: 1 }),
(a, b) => Pnt2.dist({ x: a.phi, y: a.theta }, { x: b.phi, y: b.theta }) < 0.0001,
),
).subscribe(s => {
this.spherical.theta = s.theta;
this.spherical.phi = s.phi;
});
} else {
mouseDelta$.subscribe(delta => {
this.spherical.theta -= (delta.x * (this.options.orbiting as any).sensitivityX) / 1000;
this.spherical.phi -= (delta.y * (this.options.orbiting as any).sensitivityY) / 1000;
this.spherical.phi = Math.max(0.000001, Math.min(Math.PI - 0.000001, this.spherical.phi));
});
}
}
if (this.options.zooming) {
this.mouseInput.wheel$.pipe(takeUntil(this._onRemoved$)).subscribe(delta => {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/base/pipes/gg-elastic.pipe.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { filter, map, Observable, OperatorFunction, scan, switchMap } from 'rxjs';

export default function ggElastic<T>(
export function ggElastic<T>(
tick$: Observable<[number, number]>, // [elapsed, delta]
elasticity: number, // Elasticity parameter
mix: (a: T, b: T, factor: number) => T, // Mixing function
Expand Down

0 comments on commit 71d653d

Please sign in to comment.