-
-
Notifications
You must be signed in to change notification settings - Fork 3.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
implementation of p5.Vector slerp function #6222
Changes from 8 commits
6b19f4d
ce609ab
0c191b7
e9d50ac
505d902
1ff3dff
a2165a1
87b4f06
8f6e48a
140c967
c0caa7a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1742,6 +1742,156 @@ p5.Vector = class { | |
return this; | ||
} | ||
|
||
/** | ||
* Performs spherical linear interpolation with the other vector | ||
* and returns the resulting vector. | ||
* The result of slerping between 2D vectors is always a 2D vector. | ||
* | ||
* @method slerp | ||
* @param {p5.Vector} v the p5.Vector to slerp to | ||
* @param {Number} amt The amount of interpolation. some value between 0.0 | ||
* (old vector) and 1.0 (new vector). 0.9 is very near | ||
* the new vector. 0.5 is halfway in between. | ||
* @return {p5.Vector} | ||
* | ||
* @example | ||
* <div class="norender"> | ||
* <code> | ||
* | ||
* const v1 = createVector(1, 0, 0); | ||
* const v2 = createVector(0, 1, 0); | ||
* | ||
* const v = v1.slerp(v2, 1/3); | ||
* print(v.toString()); | ||
* // v's components are almost [cos(30°), sin(30°), 0] | ||
* </code> | ||
* </div> | ||
* | ||
* <div> | ||
* <code> | ||
* let needle; | ||
* function setup() { | ||
* createCanvas(100, 100); | ||
* stroke(0); | ||
* strokeWeight(4); | ||
* | ||
* needle = createVector(50, 0); | ||
* } | ||
* | ||
* function draw(){ | ||
* background(255); | ||
* translate(50, 50); | ||
* | ||
* const theta = Math.atan2(mouseY-50, mouseX-50); | ||
* const v = createVector(50 * Math.cos(theta), 50 * Math.sin(theta)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be a little clearer here if we do There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's true that shorter is better, so I'll fix it! |
||
* // slerp between v and needle | ||
* // needle vector is changed by slerp function. | ||
* needle.slerp(v, 0.05); | ||
* | ||
* line(0, 0, needle.x, needle.y); | ||
* } | ||
* </code> | ||
* </div> | ||
* | ||
* <div> | ||
* <code> | ||
* let v1, v2, v3; | ||
* function setup(){ | ||
* createCanvas(100, 100, WEBGL); | ||
* noStroke(); | ||
* v1 = createVector(30, 0, 0); | ||
* v2 = createVector(0, 30, 0); | ||
* v3 = createVector(0, 0, 30); | ||
* } | ||
* | ||
* function draw(){ | ||
* background(0); | ||
* lights(); | ||
* fill(255); | ||
* ambientMaterial(255); | ||
* | ||
* const t = (frameCount % 60) / 60; | ||
* // v1, v2, v3 is not changed by slerp function. | ||
* // because this function is static version. | ||
* const v4 = p5.Vector.slerp(v1, v2, t); | ||
* const v5 = p5.Vector.slerp(v2, v3, t); | ||
* const v6 = p5.Vector.slerp(v3, v1, t); | ||
* translate(v4.x, v4.y, v4.z); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For this example, I wonder if we can make what's happening a bit clearer:
As a quick test, do you think something like this is clearer? https://editor.p5js.org/davepagurek/sketches/NBexafWPU function draw(){
background(255);
const t = map(sin(frameCount/30), -1, 1, 0, 1);
// v1, v2, v3 is not changed by slerp function.
// because this function is static version.
const v4 = p5.Vector.slerp(v1, v2, t);
const v5 = p5.Vector.slerp(v2, v3, t);
const v6 = p5.Vector.slerp(v3, v1, t);
strokeWeight(10)
strokeCap(SQUARE)
stroke('red')
line(0, 0, 0, v4.x, v4.y, v4.z);
stroke('green')
line(0, 0, 0, v5.x, v5.y, v5.z);
stroke('blue')
line(0, 0, 0, v6.x, v6.y, v6.z);
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought that example might be a little confusing for me too. It's true that "cylinders" or "lines" are easier to understand, so I'm going to rewrite them. I would like to adopt the "lines" because it is easier for the code to understand. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about this...? function setup(){
createCanvas(100, 100, WEBGL);
}
function draw(){
background(255);
const vx = createVector(30, 0, 0);
const vy = createVector(0, 30, 0);
const vz = createVector(0, 0, 30);
const t = map(sin(frameCount * TAU / 120), -1, 1, 0, 1);
// v1, v2, v3 is not changed by slerp function.
// because this function is static version.
const vSlerpXY = p5.Vector.slerp(vx, vy, t);
const vSlerpYZ = p5.Vector.slerp(vy, vz, t);
const vSlerpZX = p5.Vector.slerp(vz, vx, t);
strokeWeight(6);
strokeCap(SQUARE);
stroke("red");
line(0, 0, 0, vSlerpXY.x, vSlerpXY.y, vSlerpXY.z);
stroke("green");
line(0, 0, 0, vSlerpYZ.x, vSlerpYZ.y, vSlerpYZ.z);
stroke("blue");
line(0, 0, 0, vSlerpZX.x, vSlerpZX.y, vSlerpZX.z);
} Renamed variables to make it clearer which direction the axis is pointing. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. looks great! |
||
* sphere(5); | ||
* translate(v5.x - v4.x, v5.y - v4.y, v5.z - v4.z); | ||
* sphere(5); | ||
* translate(v6.x - v5.x, v6.y - v5.y, v6.z - v5.z); | ||
* sphere(5); | ||
* } | ||
* </code> | ||
* </div> | ||
*/ | ||
slerp(v, amt) { | ||
// edge cases. | ||
if (amt === 0) { return this; } | ||
if (amt === 1) { return this.set(v); } | ||
|
||
// calculate magnitudes | ||
const selfMag = this.mag(); | ||
const vMag = v.mag(); | ||
const magmag = selfMag * vMag; | ||
// if either is a zero vector, linearly interpolate by these vectors | ||
if (magmag === 0) { | ||
this.mult(1 - amt).add(v.x * amt, v.y * amt, v.z * amt); | ||
return this; | ||
} | ||
// the cross product of 'this' and 'v' is the axis of rotation | ||
const axis = this.cross(v); | ||
const axisMag = axis.mag(); | ||
// Calculates the angle between 'this' and 'v' | ||
const theta = Math.atan2(axisMag, this.dot(v)); | ||
|
||
// However, if the norm of axis is 0, normalization cannot be performed, | ||
// so we will divide the cases | ||
if (axisMag > 0) { | ||
axis.x /= axisMag; | ||
axis.y /= axisMag; | ||
axis.z /= axisMag; | ||
} else if (theta < Math.PI * 0.5) { | ||
// if the norm is 0 and the angle is less than PI/2, | ||
// the angle is very close to 0, so do linear interpolation. | ||
this.mult(1 - amt).add(v.x * amt, v.y * amt, v.z * amt); | ||
return this; | ||
} else { | ||
// If the norm is 0 and the angle is more than PI/2, the angle is | ||
// very close to PI. | ||
// In this case v can be regarded as '-this', so take any vector | ||
// that is orthogonal to 'this' and use that as the axis. | ||
if (this.z === 0 && v.z === 0) { | ||
// if both this and v are 2D vectors, use (0,0,1) | ||
// this makes the result also a 2D vector. | ||
axis.set(0, 0, 1); | ||
} else if (this.x !== 0) { | ||
// if the x components is not 0, use (y, -x, 0) | ||
axis.set(this.y, -this.x, 0).normalize(); | ||
} else { | ||
// if the x components is 0, use (1,0,0) | ||
axis.set(1, 0, 0); | ||
} | ||
} | ||
|
||
// Since 'axis' is a unit vector, ey is a vector of the same length as 'result'. | ||
const ey = axis.cross(this); | ||
// interpolate the length with 'this' and 'v'. | ||
const lerpedMagFactor = (1 - amt) + amt * vMag / selfMag; | ||
// imagine an orthonormal basis where "axis", "result" and "ey" are | ||
// the unit vectors of the z, x and y axes respectively. | ||
// rotates "result" around "axis" by t*angle towards "ey". | ||
const cosMultiplier = lerpedMagFactor * Math.cos(amt * theta); | ||
const sinMultiplier = lerpedMagFactor * Math.sin(amt * theta); | ||
// then, calculate 'result'. | ||
this.x = this.x * cosMultiplier + ey.x * sinMultiplier; | ||
this.y = this.y * cosMultiplier + ey.y * sinMultiplier; | ||
this.z = this.z * cosMultiplier + ey.z * sinMultiplier; | ||
|
||
return this; | ||
} | ||
|
||
/** | ||
* Reflect a vector about a normal to a line in 2D, or about a normal to a | ||
* plane in 3D. | ||
|
@@ -2358,6 +2508,36 @@ p5.Vector = class { | |
return target; | ||
} | ||
|
||
/** | ||
* Performs spherical linear interpolation with the other vector | ||
* and returns the resulting vector. | ||
* The result of slerping between 2D vectors is always a 2D vector. | ||
*/ | ||
/** | ||
* @method slerp | ||
* @static | ||
* @param {p5.Vector} v1 old vector | ||
* @param {p5.Vector} v2 new vectpr | ||
* @param {Number} amt | ||
* @param {p5.Vector} [target] The vector to receive the result | ||
* @return {p5.Vector} slerped vector between v1 and v2 | ||
*/ | ||
static slerp(v1, v2, amt, target) { | ||
if (!target) { | ||
target = v1.copy(); | ||
if (arguments.length === 4) { | ||
p5._friendlyError( | ||
'The target parameter is undefined, it should be of type p5.Vector', | ||
'p5.Vector.slerp' | ||
); | ||
} | ||
} else { | ||
target.set(v1); | ||
} | ||
target.slerp(v2, amt); | ||
return target; | ||
} | ||
|
||
/** | ||
* Calculates the magnitude (length) of the vector and returns the result as | ||
* a float (this is simply the equation `sqrt(x*x + y*y + z*z)`.) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By only mentioning 2D vectors in the description, I worry people might incorrectly assume it's only for 2D. Maybe we can say "This works in both 3D and 2D" before this sentence?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Spherical linear interpolation is usually used in 3D, so I didn't mention it, but I would like to describe it if necessary (I wanted to emphasize that the result is 2D even if each 2D vectors are pointered directions oppositely).