diff --git a/src/app.js b/src/app.js
index 1bbb7d1be2..672acc6142 100644
--- a/src/app.js
+++ b/src/app.js
@@ -91,6 +91,7 @@ import './webgl/p5.Camera';
import './webgl/p5.DataArray';
import './webgl/p5.Geometry';
import './webgl/p5.Matrix';
+import './webgl/p5.Quat';
import './webgl/p5.RendererGL.Immediate';
import './webgl/p5.RendererGL';
import './webgl/p5.RendererGL.Retained';
diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js
index 703cfa6cda..0b48490258 100644
--- a/src/math/p5.Vector.js
+++ b/src/math/p5.Vector.js
@@ -3887,5 +3887,33 @@ p5.Vector = class {
}
return v.equals(v2);
}
-};
-export default p5.Vector;
+
+
+ /**
+ * Replaces the components of a p5.Vector that are very close to zero with zero.
+ *
+ * In computers, handling numbers with decimals can give slightly imprecise answers due to the way those numbers are represented.
+ * This can make it hard to check if a number is zero, as it may be close but not exactly zero.
+ * This method rounds very close numbers to zero to make those checks easier
+ *
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/EPSILON
+ *
+ * @method clampToZero
+ * @return {p5.Vector} with components very close to zero replaced with zero.
+ * @chainable
+ */
+ clampToZero() {
+ this.x = this._clampToZero(this.x);
+ this.y = this._clampToZero(this.y);
+ this.z = this._clampToZero(this.z);
+ return this;
+ }
+
+ /**
+ * Helper function for clampToZero
+ * @private
+ */
+ _clampToZero(val) {
+ return Math.abs((val||0) - 0) <= Number.EPSILON ? 0 : val;
+ }
+};export default p5.Vector;
diff --git a/src/webgl/p5.Camera.js b/src/webgl/p5.Camera.js
index abdc4b4c2b..9056f2e63c 100644
--- a/src/webgl/p5.Camera.js
+++ b/src/webgl/p5.Camera.js
@@ -2460,6 +2460,84 @@ p5.Camera = class Camera {
);
}
+ /**
+ * Rotates the camera in a clockwise/counter-clockwise direction.
+ *
+ * Rolling rotates the camera without changing its orientation. The rotation
+ * happens in the camera’s "local" space.
+ *
+ * The parameter, `angle`, is the angle the camera should rotate. Passing a
+ * positive angle, as in `myCamera.roll(0.001)`, rotates the camera in counter-clockwise direction.
+ * Passing a negative angle, as in `myCamera.roll(-0.001)`, rotates the
+ * camera in clockwise direction.
+ *
+ * Note: Angles are interpreted based on the current
+ * angleMode().
+ *
+ * @method roll
+ * @param {Number} angle amount to rotate camera in current
+ * angleMode units.
+ * @example
+ *
+ *
+ * let cam;
+ * let delta = 0.01;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ * normalMaterial();
+ * // Create a p5.Camera object.
+ * cam = createCamera();
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Roll camera according to angle 'delta'
+ * cam.roll(delta);
+ *
+ * translate(0, 0, 0);
+ * box(20);
+ * translate(0, 25, 0);
+ * box(20);
+ * translate(0, 26, 0);
+ * box(20);
+ * translate(0, 27, 0);
+ * box(20);
+ * translate(0, 28, 0);
+ * box(20);
+ * translate(0,29, 0);
+ * box(20);
+ * translate(0, 30, 0);
+ * box(20);
+ * }
+ *
+ *
+ *
+ * @alt
+ * camera view rotates in counter clockwise direction with vertically stacked boxes in front of it.
+ */
+ roll(amount) {
+ const local = this._getLocalAxes();
+ const axisQuaternion = p5.Quat.fromAxisAngle(
+ this._renderer._pInst._toRadians(amount),
+ local.z[0], local.z[1], local.z[2]);
+ // const upQuat = new p5.Quat(0, this.upX, this.upY, this.upZ);
+ const newUpVector = axisQuaternion.rotateVector(
+ new p5.Vector(this.upX, this.upY, this.upZ));
+ this.camera(
+ this.eyeX,
+ this.eyeY,
+ this.eyeZ,
+ this.centerX,
+ this.centerY,
+ this.centerZ,
+ newUpVector.x,
+ newUpVector.y,
+ newUpVector.z
+ );
+ }
+
/**
* Rotates the camera left and right.
*
diff --git a/src/webgl/p5.Quat.js b/src/webgl/p5.Quat.js
new file mode 100644
index 0000000000..625595ccc0
--- /dev/null
+++ b/src/webgl/p5.Quat.js
@@ -0,0 +1,96 @@
+/**
+ * @module Math
+ * @submodule Quaternion
+ */
+
+import p5 from '../core/main';
+
+/**
+ * A class to describe a Quaternion
+ * for vector rotations in the p5js webgl renderer.
+ * Please refer the following link for details on the implementation
+ * https://danceswithcode.net/engineeringnotes/quaternions/quaternions.html
+ * @class p5.Quat
+ * @constructor
+ * @param {Number} [w] Scalar part of the quaternion
+ * @param {Number} [x] x component of imaginary part of quaternion
+ * @param {Number} [y] y component of imaginary part of quaternion
+ * @param {Number} [z] z component of imaginary part of quaternion
+ * @private
+ */
+p5.Quat = class {
+ constructor(w, x, y, z) {
+ this.w = w;
+ this.vec = new p5.Vector(x, y, z);
+ }
+
+ /**
+ * Returns a Quaternion for the
+ * axis angle representation of the rotation
+ *
+ * @method fromAxisAngle
+ * @param {Number} [angle] Angle with which the points needs to be rotated
+ * @param {Number} [x] x component of the axis vector
+ * @param {Number} [y] y component of the axis vector
+ * @param {Number} [z] z component of the axis vector
+ * @chainable
+ */
+ static fromAxisAngle(angle, x, y, z) {
+ const w = Math.cos(angle/2);
+ const vec = new p5.Vector(x, y, z).normalize().mult(Math.sin(angle/2));
+ return new p5.Quat(w, vec.x, vec.y, vec.z);
+ }
+
+ conjugate() {
+ return new p5.Quat(this.w, -this.vec.x, -this.vec.y, -this.vec.z);
+ }
+
+ /**
+ * Multiplies a quaternion with other quaternion.
+ * @method mult
+ * @param {p5.Quat} [quat] quaternion to multiply with the quaternion calling the method.
+ * @chainable
+ */
+ multiply(quat) {
+ /* eslint-disable max-len */
+ return new p5.Quat(
+ this.w * quat.w - this.vec.x * quat.vec.x - this.vec.y * quat.vec.y - this.vec.z - quat.vec.z,
+ this.w * quat.vec.x + this.vec.x * quat.w + this.vec.y * quat.vec.z - this.vec.z * quat.vec.y,
+ this.w * quat.vec.y - this.vec.x * quat.vec.z + this.vec.y * quat.w + this.vec.z * quat.vec.x,
+ this.w * quat.vec.z + this.vec.x * quat.vec.y - this.vec.y * quat.vec.x + this.vec.z * quat.w
+ );
+ /* eslint-enable max-len */
+ }
+
+ /**
+ * This is similar to quaternion multiplication
+ * but when multipying vector with quaternion
+ * the multiplication can be simplified to the below formula.
+ * This was taken from the below stackexchange link
+ * https://gamedev.stackexchange.com/questions/28395/rotating-vector3-by-a-quaternion/50545#50545
+ * @param {p5.Vector} [p] vector to rotate on the axis quaternion
+ * @returns
+ */
+ rotateVector(p) {
+ return new p5.Vector.mult( p, this.w*this.w - this.vec.dot(this.vec) )
+ .add( p5.Vector.mult( this.vec, 2 * p.dot(this.vec) ) )
+ .add( p5.Vector.mult( this.vec, 2 * this.w ).cross( p ) )
+ .clampToZero();
+ }
+
+ /**
+ * Rotates the Quaternion by the quaternion passed
+ * which contains the axis of roation and angle of rotation
+ *
+ * @method rotateBy
+ * @param {p5.Quat} [axesQuat] axis quaternion which contains
+ * the axis of rotation and angle of rotation
+ * @chainable
+ */
+ rotateBy(axesQuat) {
+ return axesQuat.multiply(this).multiply(axesQuat.conjugate()).
+ vec.clampToZero();
+ }
+};
+
+export default p5.Quat;
diff --git a/test/unit/webgl/p5.Camera.js b/test/unit/webgl/p5.Camera.js
index 96a5165d14..b746a5053e 100644
--- a/test/unit/webgl/p5.Camera.js
+++ b/test/unit/webgl/p5.Camera.js
@@ -1,3 +1,5 @@
+import { HALF_PI } from '../../../src/core/constants';
+
suite('p5.Camera', function() {
var myp5;
var myCam;
@@ -194,6 +196,69 @@ suite('p5.Camera', function() {
assert.strictEqual(myCam.eyeZ, orig.ez, 'eye Z pos changed');
});
+ test('Roll() with positive parameter sets correct Matrix w/o \
+ changing eyeXYZ', function() {
+ var orig = getVals(myCam);
+
+ var expectedMatrix = new Float32Array([
+ 0, -1, 0, 0,
+ 1, 0, 0, 0,
+ 0, 0, 1, 0,
+ 0, 0, -86.6025390625, 1
+ ]);
+
+ myCam.roll(HALF_PI);
+
+ assert.deepEqual(myCam.cameraMatrix.mat4, expectedMatrix);
+
+ assert.strictEqual(myCam.eyeX, orig.ex, 'eye X pos changed');
+ assert.strictEqual(myCam.eyeY, orig.ey, 'eye Y pos changed');
+ assert.strictEqual(myCam.eyeZ, orig.ez, 'eye Z pos changed');
+ });
+
+ test('Roll() with negative parameter sets correct matrix w/o \
+ changing eyeXYZ', function() {
+ var orig = getVals(myCam);
+
+ var expectedMatrix = new Float32Array([
+ 0, 1, 0, 0,
+ -1, 0, 0, 0,
+ 0, 0, 1, 0,
+ 0, 0, -86.6025390625, 1
+ ]);
+
+ myCam.tilt(HALF_PI);
+
+ assert.deepEqual(myCam.cameraMatrix.mat4, expectedMatrix);
+
+ assert.strictEqual(myCam.eyeX, orig.ex, 'eye X pos changed');
+ assert.strictEqual(myCam.eyeY, orig.ey, 'eye Y pos changed');
+ assert.strictEqual(myCam.eyeZ, orig.ez, 'eye Z pos changed');
+ });
+
+ test('Roll(0) sets correct matrix w/o changing upXYZ and eyeXYZ', function() {
+ var orig = getVals(myCam);
+
+ var expectedMatrix = new Float32Array([
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 0, 0, -86.6025390625, 1
+ ]);
+
+ myCam.roll(0);
+
+ assert.deepEqual(myCam.cameraMatrix.mat4, expectedMatrix);
+
+ assert.strictEqual(myCam.eyeX, orig.ex, 'eye X pos changed');
+ assert.strictEqual(myCam.eyeY, orig.ey, 'eye Y pos changed');
+ assert.strictEqual(myCam.eyeZ, orig.ez, 'eye Z pos changed');
+
+ assert.strictEqual(myCam.upX, orig.ux, 'up X pos changed');
+ assert.strictEqual(myCam.upY, orig.uy, 'up Y pos changed');
+ assert.strictEqual(myCam.upZ, orig.uz, 'up Z pos changed');
+ });
+
test('LookAt() should set centerXYZ without changing eyeXYZ or \
upXYZ', function() {
var orig = getVals(myCam);
@@ -266,6 +331,26 @@ suite('p5.Camera', function() {
assert.strictEqual(myCam.eyeY, orig.ey, 'eye Y pos changed');
assert.strictEqual(myCam.eyeZ, orig.ez, 'eye Z pos changed');
});
+
+ test('Roll() with positive parameter sets correct Matrix w/o \
+ changing eyeXYZ', function() {
+ var orig = getVals(myCam);
+
+ var expectedMatrix = new Float32Array([
+ 0, -1, 0, 0,
+ 1, 0, 0, 0,
+ 0, 0, 1, 0,
+ 0, 0, -86.6025390625, 1
+ ]);
+
+ myCam.roll(90);
+
+ assert.deepEqual(myCam.cameraMatrix.mat4, expectedMatrix);
+
+ assert.strictEqual(myCam.eyeX, orig.eyeX, 'eye X pos changed');
+ assert.strictEqual(myCam.eyeY, orig.eyeY, 'eye Y pos changed');
+ assert.strictEqual(myCam.eyeZ, orig.eyeZ, 'eye Z pos changed');
+ });
});
suite('Position / Orientation', function() {