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() {