From de908648d811f4e7cd373f86806d22d8d6d8d1f8 Mon Sep 17 00:00:00 2001
From: KPal <48248865+kpal81xd@users.noreply.github.com>
Date: Fri, 7 Feb 2025 14:12:49 +0000
Subject: [PATCH] Camera controls fixes (#7337)
* Adds a reset to multi orbit and fly
* Fixed excess spinning when resetting focus
* Changed flying/orbit switching to occur on mouse down and force switch for orbit only actions (focus, zoom)
---
examples/src/examples/camera/fly.example.mjs | 4 +-
.../src/examples/camera/multi.example.mjs | 33 ++++---
.../src/examples/camera/orbit.example.mjs | 33 ++++---
scripts/esm/camera-controls.mjs | 89 +++++++++++++++----
4 files changed, 119 insertions(+), 40 deletions(-)
diff --git a/examples/src/examples/camera/fly.example.mjs b/examples/src/examples/camera/fly.example.mjs
index dc5ebadf0f4..f327669cb6c 100644
--- a/examples/src/examples/camera/fly.example.mjs
+++ b/examples/src/examples/camera/fly.example.mjs
@@ -82,10 +82,12 @@ const calcEntityAABB = (bbox, entity) => {
* @returns {CameraControls} The camera-controls script.
*/
const createFlyCamera = (focus) => {
+ const start = new pc.Vec3(0, 20, 30);
+
const camera = new pc.Entity();
camera.addComponent('camera');
camera.addComponent('script');
- camera.setPosition(0, 20, 30);
+ camera.setPosition(start);
app.root.addChild(camera);
const bbox = calcEntityAABB(new pc.BoundingBox(), focus);
diff --git a/examples/src/examples/camera/multi.example.mjs b/examples/src/examples/camera/multi.example.mjs
index 52ea3a90cfc..17dd3faa8f6 100644
--- a/examples/src/examples/camera/multi.example.mjs
+++ b/examples/src/examples/camera/multi.example.mjs
@@ -1,4 +1,4 @@
-// @config DESCRIPTION
(WASDQE) Move (Fly enabled)
(LMB) Orbit, (LMB (Orbit disabled) / RMB) Fly
(Hold Shift / MMB / RMB (Fly or Orbit disabled)) Pan
(Scroll Wheel (Orbit or Pan enabled)) Zoom
(F) Focus
+// @config DESCRIPTION (WASDQE) Move (Fly enabled)
(LMB) Orbit, (LMB (Orbit disabled) / RMB) Fly
(Hold Shift / MMB / RMB (Fly or Orbit disabled)) Pan
(Scroll Wheel (Orbit or Pan enabled)) Zoom
(F) Focus (R) Reset
import { data } from 'examples/observer';
import { deviceType, rootPath, fileImport } from 'examples/utils';
import * as pc from 'playcanvas';
@@ -84,14 +84,16 @@ const calcEntityAABB = (bbox, entity) => {
* @returns {CameraControls} The camera-controls script.
*/
const createMultiCamera = (focus) => {
+ const start = new pc.Vec3(0, 20, 30);
+
const camera = new pc.Entity();
camera.addComponent('camera');
camera.addComponent('script');
- camera.setPosition(0, 20, 30);
+ camera.setPosition(start);
app.root.addChild(camera);
const bbox = calcEntityAABB(new pc.BoundingBox(), focus);
- const cameraDist = camera.getPosition().distance(bbox.center);
+ const cameraDist = start.distance(bbox.center);
/** @type {CameraControls} */
const script = camera.script.create(CameraControls, {
@@ -103,13 +105,24 @@ const createMultiCamera = (focus) => {
// focus on entity when 'f' key is pressed
const onKeyDown = (/** @type {KeyboardEvent} */ e) => {
- if (e.key === 'f') {
- script.refocus(
- bbox.center,
- null,
- data.get('example.zoomReset') ? cameraDist : null,
- data.get('example.smoothedFocus')
- );
+ switch (e.key) {
+ case 'f': {
+ script.refocus(
+ bbox.center,
+ null,
+ data.get('example.zoomReset') ? cameraDist : null,
+ data.get('example.smoothedFocus')
+ );
+ break;
+ }
+ case 'r': {
+ script.refocus(
+ bbox.center,
+ start,
+ data.get('example.zoomReset') ? cameraDist : null,
+ data.get('example.smoothedFocus')
+ );
+ }
}
};
window.addEventListener('keydown', onKeyDown);
diff --git a/examples/src/examples/camera/orbit.example.mjs b/examples/src/examples/camera/orbit.example.mjs
index 1da2470b5fc..9511fdee73f 100644
--- a/examples/src/examples/camera/orbit.example.mjs
+++ b/examples/src/examples/camera/orbit.example.mjs
@@ -1,4 +1,4 @@
-// @config DESCRIPTION (LMB) Orbit
(Hold Shift / MMB / RMB ) Pan
(Scroll Wheel) Zoom
(F) Focus
+// @config DESCRIPTION (LMB) Orbit
(Hold Shift / MMB / RMB ) Pan
(Scroll Wheel) Zoom
(F) Focus (R) Reset
import { data } from 'examples/observer';
import { deviceType, rootPath, fileImport } from 'examples/utils';
import * as pc from 'playcanvas';
@@ -82,14 +82,16 @@ const calcEntityAABB = (bbox, entity) => {
* @returns {CameraControls} The camera-controls script.
*/
const createOrbitCamera = (focus) => {
+ const start = new pc.Vec3(0, 20, 30);
+
const camera = new pc.Entity();
camera.addComponent('camera');
camera.addComponent('script');
- camera.setPosition(0, 20, 30);
+ camera.setPosition(start);
app.root.addChild(camera);
const bbox = calcEntityAABB(new pc.BoundingBox(), focus);
- const cameraDist = camera.getPosition().distance(bbox.center);
+ const cameraDist = start.distance(bbox.center);
/** @type {CameraControls} */
const script = camera.script.create(CameraControls, {
@@ -102,13 +104,24 @@ const createOrbitCamera = (focus) => {
// focus on entity when 'f' key is pressed
const onKeyDown = (/** @type {KeyboardEvent} */ e) => {
- if (e.key === 'f') {
- script.refocus(
- bbox.center,
- null,
- data.get('example.zoomReset') ? cameraDist : null,
- data.get('example.smoothedFocus')
- );
+ switch (e.key) {
+ case 'f': {
+ script.refocus(
+ bbox.center,
+ null,
+ data.get('example.zoomReset') ? cameraDist : null,
+ data.get('example.smoothedFocus')
+ );
+ break;
+ }
+ case 'r': {
+ script.refocus(
+ bbox.center,
+ start,
+ data.get('example.zoomReset') ? cameraDist : null,
+ data.get('example.smoothedFocus')
+ );
+ }
}
};
window.addEventListener('keydown', onKeyDown);
diff --git a/scripts/esm/camera-controls.mjs b/scripts/esm/camera-controls.mjs
index 3dfae4aca51..c94bf14bfad 100644
--- a/scripts/esm/camera-controls.mjs
+++ b/scripts/esm/camera-controls.mjs
@@ -137,7 +137,13 @@ class CameraControls extends Script {
* @type {boolean}
* @private
*/
- _orbiting = false;
+ _dragging = false;
+
+ /**
+ * @type {boolean}
+ * @private
+ */
+ _orbiting = true;
/**
* @type {boolean}
@@ -556,6 +562,43 @@ class CameraControls extends Script {
return event.button === 0;
}
+ /**
+ * @private
+ * @returns {boolean} Whether the switch to orbit was successful.
+ */
+ _switchToOrbit() {
+ if (!this.enableOrbit) {
+ return false;
+ }
+ if (this._flying) {
+ this._flying = false;
+ this._focusDir(tmpV1);
+ this._origin.add(tmpV1);
+ this._position.add(tmpV1);
+ }
+ this._orbiting = true;
+ return true;
+ }
+
+ /**
+ * @private
+ * @returns {boolean} Whether the switch to fly was successful.
+ */
+ _switchToFly() {
+ if (!this.enableFly) {
+ return false;
+ }
+ if (this._orbiting) {
+ this._orbiting = false;
+ this._zoomDist = this._cameraDist;
+ this._origin.copy(this.entity.getPosition());
+ this._position.copy(this._origin);
+ this._cameraTransform.setTranslate(0, 0, 0);
+ }
+ this._flying = true;
+ return true;
+ }
+
/**
* @private
* @param {PointerEvent} event - The pointer event.
@@ -587,18 +630,17 @@ class CameraControls extends Script {
// start mouse pan
this._lastPosition.set(event.clientX, event.clientY);
this._panning = true;
+ this._dragging = true;
}
if (startFly) {
// start fly
- this._zoomDist = this._cameraDist;
- this._origin.copy(this.entity.getPosition());
- this._position.copy(this._origin);
- this._cameraTransform.setTranslate(0, 0, 0);
- this._flying = true;
+ this._switchToFly();
+ this._dragging = true;
}
if (startOrbit) {
// start orbit
- this._orbiting = true;
+ this._switchToOrbit();
+ this._dragging = true;
}
}
@@ -612,6 +654,11 @@ class CameraControls extends Script {
}
this._pointerEvents.set(event.pointerId, event);
+ if (this._focusing) {
+ this._cancelSmoothTransform();
+ this._focusing = false;
+ }
+
if (this._pointerEvents.size === 1) {
if (this._panning) {
// mouse pan
@@ -649,17 +696,11 @@ class CameraControls extends Script {
this._lastPinchDist = -1;
this._panning = false;
}
- if (this._orbiting) {
- this._orbiting = false;
- }
if (this._panning) {
this._panning = false;
}
- if (this._flying) {
- this._focusDir(tmpV1);
- this._origin.add(tmpV1);
- this._position.add(tmpV1);
- this._flying = false;
+ if (this._dragging) {
+ this._dragging = false;
}
}
@@ -882,7 +923,12 @@ class CameraControls extends Script {
return;
}
if (this._flying) {
- return;
+ if (this._dragging) {
+ return;
+ }
+ if (!this._switchToOrbit()) {
+ return;
+ }
}
if (!this._camera) {
@@ -911,8 +957,8 @@ class CameraControls extends Script {
_smoothTransform(dt) {
const ar = dt === -1 ? 1 : lerpRate(this._focusing ? this.focusDamping : this.rotateDamping, dt);
const am = dt === -1 ? 1 : lerpRate(this._focusing ? this.focusDamping : this.moveDamping, dt);
- this._angles.x = math.lerp(this._angles.x, this._dir.x, ar);
- this._angles.y = math.lerp(this._angles.y, this._dir.y, ar);
+ this._angles.x = math.lerpAngle(this._angles.x % 360, this._dir.x % 360, ar);
+ this._angles.y = math.lerpAngle(this._angles.y % 360, this._dir.y % 360, ar);
this._position.lerp(this._position, this._origin, am);
this._baseTransform.setTRS(this._position, tmpQ1.setFromEulerAngles(this._angles), Vec3.ONE);
@@ -960,7 +1006,12 @@ class CameraControls extends Script {
return;
}
if (this._flying) {
- return;
+ if (this._dragging) {
+ return;
+ }
+ if (!this._switchToOrbit()) {
+ return;
+ }
}
if (start) {