Skip to content

Commit

Permalink
Camera controllers adjustments (#27)
Browse files Browse the repository at this point in the history
* fixed stuck mouse drag state when releasing mouse over other element

* fixed orbit camera controller work in inactive state; implemented elastic motion reset on activate/deactivate controller

* reflected changes to free camera controller

* fixed typo
  • Loading branch information
AndyGura authored Dec 11, 2024
1 parent 433cacf commit 675b524
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 47 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BehaviorSubject, combineLatest, filter, takeUntil } from 'rxjs';
import { BehaviorSubject, combineLatest, filter, Subject, takeUntil } from 'rxjs';
import {
DirectionKeyboardInput,
DirectionKeyboardKeymap,
Expand Down Expand Up @@ -91,6 +91,21 @@ export class FreeCameraController extends IEntity {
*/
public readonly directionsInput: DirectionKeyboardInput;

protected spherical: MutableSpherical = { phi: 0, radius: 1, theta: 0 };

get active(): boolean {
return super.active;
}

set active(value: boolean) {
if (!super.active && value) {
this.reset();
}
super.active = value;
}

protected resetMotion$: Subject<void> = new Subject<void>();

/**
* Creates a new FreeCameraInput instance.
* @param keyboard The keyboard input controller to use for camera movement.
Expand All @@ -117,8 +132,14 @@ export class FreeCameraController extends IEntity {
this.directionsInput = new DirectionKeyboardInput(keyboard, this.options.keymap);
}

public reset(): void {
this.spherical = Pnt3.toSpherical(Pnt3.rot({ x: 0, y: 0, z: -1 }, this.camera.rotation));
this.resetMotion$.next();
}

async onSpawned(world: GgWorld<any, any>): Promise<void> {
await super.onSpawned(world);
this.spherical = Pnt3.toSpherical(Pnt3.rot({ x: 0, y: 0, z: -1 }, this.camera.rotation));
// Subscribe to keyboard input for movement controls
const keys = ['KeyE', 'KeyQ'];
if (this.camera.camera.supportsFov) {
Expand Down Expand Up @@ -166,14 +187,17 @@ export class FreeCameraController extends IEntity {
});

// Subscribe to mouse input for camera rotation
const spherical: MutableSpherical = Pnt3.toSpherical(Pnt3.rot({ x: 0, y: 0, z: -1 }, this.camera.rotation));
let isTouchScreen = MouseInput.isTouchDevice();
let mouseDelta$ = this.mouseInput.delta$.pipe(
takeUntil(this._onRemoved$),
filter(() => isTouchScreen || !this.options.ignoreMouseUnlessPointerLocked || this.mouseInput.isPointerLocked),
filter(
() =>
this.active &&
(isTouchScreen || !this.options.ignoreMouseUnlessPointerLocked || this.mouseInput.isPointerLocked),
),
);
if (this.options.cameraRotationElasticity > 0) {
const s$: BehaviorSubject<Spherical> = new BehaviorSubject(spherical);
const s$: BehaviorSubject<Spherical> = new BehaviorSubject(this.spherical);
mouseDelta$.subscribe(delta => {
const s = s$.getValue();
s$.next({
Expand All @@ -185,41 +209,54 @@ export class FreeCameraController extends IEntity {
radius: 1,
});
});
s$.pipe(
takeUntil(this._onRemoved$),
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;
const startElasticMotion = () => {
s$.pipe(
takeUntil(this._onRemoved$),
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,
),
takeUntil(this.resetMotion$),
).subscribe(s => {
this.spherical.theta = s.theta;
this.spherical.phi = s.phi;
});
};
this.resetMotion$.pipe(takeUntil(this._onRemoved$)).subscribe(() => {
s$.next(this.spherical);
startElasticMotion();
});
startElasticMotion();
} 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));
this.spherical.theta -= (delta.x * this.options.cameraRotationSensitivity) / 1000;
this.spherical.phi += (delta.y * this.options.cameraRotationSensitivity) / 1000;
this.spherical.phi = Math.max(0.000001, Math.min(Math.PI - 0.000001, this.spherical.phi));
});
}

// Setup updating camera position and rotation based on input
this.camera.tick$.pipe(takeUntil(this._onRemoved$)).subscribe(([_, delta]) => {
this.camera.camera.fov += cameraFovInc;
this.camera.position = Pnt3.add(
this.camera.position,
Pnt3.rot(
Pnt3.scalarMult(translateVector, (this.options.cameraLinearSpeed * delta) / 1000),
this.camera.rotation,
),
);
this.camera.rotation = Qtrn.lookAt(
this.camera.position,
Pnt3.add(this.camera.position, Pnt3.fromSpherical(spherical)),
);
});
this.camera.tick$
.pipe(
takeUntil(this._onRemoved$),
filter(() => this.active),
)
.subscribe(([_, delta]) => {
this.camera.camera.fov += cameraFovInc;
this.camera.position = Pnt3.add(
this.camera.position,
Pnt3.rot(
Pnt3.scalarMult(translateVector, (this.options.cameraLinearSpeed * delta) / 1000),
this.camera.rotation,
),
);
this.camera.rotation = Qtrn.lookAt(
this.camera.position,
Pnt3.add(this.camera.position, Pnt3.fromSpherical(this.spherical)),
);
});

// start input
this.mouseInput.start();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
Spherical,
TickOrder,
} from '../../../../base';
import { BehaviorSubject, filter, takeUntil } from 'rxjs';
import { BehaviorSubject, filter, Subject, takeUntil } from 'rxjs';
import { map } from 'rxjs/operators';
import { Renderer3dEntity } from '../../renderer-3d.entity';

Expand Down Expand Up @@ -63,25 +63,44 @@ export class OrbitCameraController extends IEntity {
protected spherical: MutableSpherical = { phi: 0, radius: 10, theta: 0 };

public target: Point3 = Pnt3.O;

get active(): boolean {
return super.active;
}

set active(value: boolean) {
if (!super.active && value) {
this.reset();
}
super.active = value;
}

public get radius(): number {
return this.spherical.radius;
}

public set radius(value: number) {
this.spherical.radius = value;
}

public get phi(): number {
return this.spherical.phi;
}

public set phi(value: number) {
this.spherical.phi = Math.max(0.000001, Math.min(Math.PI - 0.000001, value));
}

public get theta(): number {
return this.spherical.theta;
}

public set theta(value: number) {
this.spherical.theta = value;
}

protected resetMotion$: Subject<void> = new Subject<void>();

constructor(
protected readonly camera: Renderer3dEntity,
options: Partial<OrbitCameraControllerOptions> = {},
Expand All @@ -91,13 +110,20 @@ export class OrbitCameraController extends IEntity {
this.mouseInput = new MouseInput(this.options.mouseOptions);
}

public reset(): void {
let targetDistance = Pnt3.dist(this.target, this.camera.position);
this.target = Pnt3.add(this.camera.position, Pnt3.rot({ x: 0, y: 0, z: -targetDistance }, this.camera.rotation));
this.spherical = Pnt3.toSpherical(Pnt3.sub(this.camera.position, this.target));
this.resetMotion$.next();
}

async onSpawned(world: GgWorld<any, any>): Promise<void> {
await super.onSpawned(world);
this.spherical = Pnt3.toSpherical(Pnt3.sub(this.camera.position, this.target));
if (this.options.orbiting) {
let mouseDelta$ = this.mouseInput.delta$.pipe(
takeUntil(this._onRemoved$),
filter(() => this.mouseInput.state == MouseInputState.DRAG),
filter(() => this.active && this.mouseInput.state == MouseInputState.DRAG),
);
if (this.options.orbitingElasticity > 0) {
const s$: BehaviorSubject<Spherical> = new BehaviorSubject(this.spherical);
Expand All @@ -112,18 +138,26 @@ export class OrbitCameraController extends IEntity {
radius: 1,
});
});
s$.pipe(
takeUntil(this._onRemoved$),
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;
const startElasticMotion = () => {
s$.pipe(
takeUntil(this._onRemoved$),
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,
),
takeUntil(this.resetMotion$),
).subscribe(s => {
this.spherical.theta = s.theta;
this.spherical.phi = s.phi;
});
};
this.resetMotion$.pipe(takeUntil(this._onRemoved$)).subscribe(() => {
s$.next(this.spherical);
startElasticMotion();
});
startElasticMotion();
} else {
mouseDelta$.subscribe(delta => {
this.spherical.theta -= (delta.x * (this.options.orbiting as any).sensitivityX) / 1000;
Expand Down Expand Up @@ -200,6 +234,7 @@ export class OrbitCameraController extends IEntity {
this.camera.tick$
.pipe(
takeUntil(this._onRemoved$),
filter(() => this.active),
map(() => this.spherical),
)
.subscribe(spherical => {
Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/base/inputs/mouse.input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ export class MouseInput extends IInput<[], [unlockPointer?: boolean]> {
if (this.options.canvas) {
this.options.canvas.releasePointerCapture(event.pointerId);
}
this._element.removeEventListener('pointerup', onPointerUp as any);
window.removeEventListener('pointerup', onPointerUp as any);
this._element.removeEventListener('pointercancel', onPointerUp as any);
}
this._state$.next(pointerLengthsStateMap[Math.min(pointers.length, 2)]);
Expand All @@ -235,7 +235,8 @@ export class MouseInput extends IInput<[], [unlockPointer?: boolean]> {
if (this.options.canvas) {
this.options.canvas.setPointerCapture(event.pointerId);
}
this._element.addEventListener('pointerup', onPointerUp as any);
// use window instead of this._element to handle case when mouse was released over other element
window.addEventListener('pointerup', onPointerUp as any);
this._element.addEventListener('pointercancel', onPointerUp as any);
} catch (err) {
console.error(err);
Expand Down

0 comments on commit 675b524

Please sign in to comment.