You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[p5.js 2.0+ Bug Report]: Vector.slerp() does not work correctly for vectors of more than three dimensions #8928
functionsetup(){letc=0.5;letv1=createVector(0,1,0,1);letv2=createVector(1,0,1,0);v1.slerp(v2,0.5);// v1 should be vector[0.707, 0.707, 0.707, 0.707] but is actually vector[0.5, 0.707, 0.5, 1]print(v1.toString());}
Discussion
slerp() currently uses cross() for its computation, which is only defined for 3D vectors. It needs to use a more general formula, which is readily available on the web; search for "slerp formula".
Feel free to adapt the following (to both the instance and static methods). In my brief testing, it gives the same result as the current slerp() for 2D and 3D vectors, and also seems to work correctly for 4D vectors. It can certainly be optimized.
This addresses the question raised above regarding whether slerp() is meaningful outside of three dimensions: it is. Spherical linear interpolation is well-defined in any number of dimensions, as it is fundamentally a rotation along the great circle connecting two unit vectors on an n-sphere, scaled by an interpolated magnitude. The current implementation is limited to 3D only because it relies on cross() to reconstruct an explicit interpolation axis, and cross() is itself a 3D-only operation (see #8925). Once the axis-based approach is replaced with one based on dot(), which is dimension-agnostic, the formula generalizes without modification.
Formula
The closed-form slerp used above is the standard one:
This depends only on dot(), normalize(), mag(), and scalar arithmetic, none of which are specific to three dimensions.
I verified this against the repro case in the issue description. With v1 = [0, 1, 0, 1], v2 = [1, 0, 1, 0], and t = 0.5, the formula produces [0.5, 0.5, 0.5, 0.5] prior to magnitude scaling, which normalizes to magnitude √2, i.e. [0.707, 0.707, 0.707, 0.707]. This matches the expected output stated in the issue.
Edge cases requiring explicit handling
Two conditions in the formula above are not self-evident from the snippet and should be addressed in any implementation:
theta === 0. When the two input vectors point in the same direction, sin(theta) is zero and appears in the denominator of both coefficients. This should be handled either by retaining the existing t = 0 / t = 1 early-return behavior already present in slerp(), or by falling through to a plain lerp() when theta is zero.
Floating-point drift in dot(). Because dot(normalize(v0), normalize(v1)) is a cosine value, it should be mathematically bounded to [-1, 1], but floating-point error can push it fractionally outside that range for near-parallel vectors. Feeding an out-of-range value directly to acos() returns NaN. The dot product should be clamped (e.g. constrain(dot, -1, 1)) before being passed to acos().
Proposed next step
I would be glad to submit a pull request implementing this. The change would update both the instance method and the static p5.Vector.slerp(), and would include a 4D test case added to the existing slerp unit tests to guard against regression.
Before doing so, I would appreciate input on the preferred handling of the theta === 0 case - specifically, whether it should fall back to lerp(), or preserve the current early-return structure used elsewhere in slerp() - so the implementation matches project conventions.
That said, I have absolutely no interest in 4D vectors, so I am not interested in this issue either. However, as the person responsible for implementing p5.Vector.slerp, I felt compelled to point out a few concerns.
If you wish to modify this function, please feel free to do so as you please.
functionslerp2(v0,v1,t){constm=v0.mag()+(v1.mag()-v0.mag())*t;constr0=p5.Vector.normalize(v0);constr1=p5.Vector.normalize(v1);consta=acos(r0.dot(r1));constb=Math.atan2(Math.sqrt(Math.pow(r0.x*r1.y-r0.y*r1.x,2)+Math.pow(r0.x*r1.z-r0.z*r1.x,2)+Math.pow(r0.y*r1.z-r0.z*r1.y,2)+Math.pow(r0.x*r1.w-r0.w*r1.x,2)+Math.pow(r0.y*r1.w-r0.w*r1.y,2)+Math.pow(r0.z*r1.w-r0.w*r1.z,2)),r0.dot(r1));console.log(Math.PI-a);console.log(Math.PI-b);// use b instead of a.if(b===0){returnp5.Vector.add(v0,p5.Vector.sub(v1,v0).mult(t));}elseif(b===Math.PI){console.log('two vector opposite error.');returnnull;}else{r0.mult(sin((1-t)*b)/sin(b));r1.mult(sin(t*b)/sin(b));returnr0.add(r1).mult(m);}}
acos() exhibits unstable behavior near 0 and Math.PI. Clamping the dot product between -1 and 1 does not improve accuracy.
Hey, @ksen0, I'd like to work on this, could you assign it to me?
Looking at the current slerp(), it's actually already using an atan2-based angle (Math.atan2(axisMag, this.dot(v))), so the numerical-stability concern raised above is largely already addressed for 3D. The part that's actually 3D-only is narrower than it first appears: cross() is used for two things, (1) getting axisMag (the sine-of-angle term) and (2) building ey = axis.cross(this), a second basis vector orthogonal to this, pointing toward v, used to do a 2D rotation within the plane spanned by this and v.
Both uses can be replaced with a Gram-Schmidt projection instead of a cross product, which is dimension-agnostic:
constr0=p5.Vector.normalize(this);constr1=p5.Vector.normalize(v);constdotVal=r0.dot(r1);// cos(theta)constproj=p5.Vector.sub(r1,p5.Vector.mult(r0,dotVal));// component of r1 orthogonal to r0constsinTheta=proj.mag();consttheta=Math.atan2(sinTheta,dotVal);
proj normalized is the generalized ey. Everything after that in the current implementation -> lerpedMagFactor, cosMultiplier/sinMultiplier, and the final this.x = this.x * cosMultiplier + ey.x * sinMultiplier combination, isn't 3D-specific and carries over unchanged, just iterated over this.values/v.values instead of x/y/z.
I verified this against the 4D repro case in the issue ([0,1,0,1], [1,0,1,0], t=0.5) and it produces the expected [0.707, 0.707, 0.707, 0.707].
Degenerate case (theta close to PI): the existing code's fallback for "axis norm is 0 and angle is near PI" picks an arbitrary vector orthogonal to this (using specific 3D constructions like (y, -x, 0)). In N dimensions there's no single obvious choice, antiparallel vectors leave an entire (N-1)-dimensional space of valid rotation planes, so any choice here is arbitrary, not a "true" generalization. I'll implement something reasonable (e.g., zero the largest-magnitude component and swap a pair of others to build an orthogonal vector), just wanted to flag this.
Dimension mismatch:dot()/mult() already truncate to the smaller vector's dimension via prioritizeSmallerDimension. I'll have slerp() follow the same convention for consistency, unless there's a preference otherwise.
I'll update both the instance and static (p5.Vector.slerp()) versions, and add 4D test cases to the existing slerp unit tests, including the near-antiparallel corner case from the thread above.
Activity
ksen0 commentedon Jun 17, 2026
@davepagurek what do you think about slerp outside of 3D? I do not know how commonly needed that is
LalitNarayanYadav commentedon Jul 2, 2026
This addresses the question raised above regarding whether
slerp()is meaningful outside of three dimensions: it is. Spherical linear interpolation is well-defined in any number of dimensions, as it is fundamentally a rotation along the great circle connecting two unit vectors on an n-sphere, scaled by an interpolated magnitude. The current implementation is limited to 3D only because it relies oncross()to reconstruct an explicit interpolation axis, andcross()is itself a 3D-only operation (see #8925). Once the axis-based approach is replaced with one based ondot(), which is dimension-agnostic, the formula generalizes without modification.Formula
The closed-form slerp used above is the standard one:
This depends only on
dot(),normalize(),mag(), and scalar arithmetic, none of which are specific to three dimensions.I verified this against the repro case in the issue description. With
v1 = [0, 1, 0, 1],v2 = [1, 0, 1, 0], andt = 0.5, the formula produces[0.5, 0.5, 0.5, 0.5]prior to magnitude scaling, which normalizes to magnitude√2, i.e.[0.707, 0.707, 0.707, 0.707]. This matches the expected output stated in the issue.Edge cases requiring explicit handling
Two conditions in the formula above are not self-evident from the snippet and should be addressed in any implementation:
theta === 0. When the two input vectors point in the same direction,sin(theta)is zero and appears in the denominator of both coefficients. This should be handled either by retaining the existingt = 0/t = 1early-return behavior already present inslerp(), or by falling through to a plainlerp()whenthetais zero.dot(). Becausedot(normalize(v0), normalize(v1))is a cosine value, it should be mathematically bounded to[-1, 1], but floating-point error can push it fractionally outside that range for near-parallel vectors. Feeding an out-of-range value directly toacos()returnsNaN. The dot product should be clamped (e.g.constrain(dot, -1, 1)) before being passed toacos().Proposed next step
I would be glad to submit a pull request implementing this. The change would update both the instance method and the static
p5.Vector.slerp(), and would include a 4D test case added to the existing slerp unit tests to guard against regression.Before doing so, I would appreciate input on the preferred handling of the
theta === 0case - specifically, whether it should fall back tolerp(), or preserve the current early-return structure used elsewhere inslerp()- so the implementation matches project conventions.inaridarkfox4231 commentedon Jul 10, 2026
What should the result of SLERP be?
slerp ?
inaridarkfox4231 commentedon Jul 10, 2026
That said, I have absolutely no interest in 4D vectors, so I am not interested in this issue either. However, as the person responsible for implementing p5.Vector.slerp, I felt compelled to point out a few concerns.
If you wish to modify this function, please feel free to do so as you please.
inaridarkfox4231 commentedon Jul 11, 2026
Improvement proposal.
slerp inspect
acos() exhibits unstable behavior near 0 and Math.PI. Clamping the dot product between -1 and 1 does not improve accuracy.
This method calculates both cosine and sine and then derives the angle from them, resulting in high angular precision.
※Returning null when two vectors point in exactly opposite directions is a provisional measure.
inaridarkfox4231 commentedon Jul 11, 2026
Also, I'm not sure if this has already been noted, but it seems array() does not support four dimensions.
Aman-goel-04 commentedon Jul 25, 2026
Hey, @ksen0, I'd like to work on this, could you assign it to me?
Looking at the current
slerp(), it's actually already using anatan2-based angle (Math.atan2(axisMag, this.dot(v))), so the numerical-stability concern raised above is largely already addressed for 3D. The part that's actually 3D-only is narrower than it first appears:cross()is used for two things, (1) gettingaxisMag(the sine-of-angle term) and (2) buildingey = axis.cross(this), a second basis vector orthogonal tothis, pointing towardv, used to do a 2D rotation within the plane spanned bythisandv.Both uses can be replaced with a Gram-Schmidt projection instead of a cross product, which is dimension-agnostic:
projnormalized is the generalizedey. Everything after that in the current implementation ->lerpedMagFactor,cosMultiplier/sinMultiplier, and the finalthis.x = this.x * cosMultiplier + ey.x * sinMultipliercombination, isn't 3D-specific and carries over unchanged, just iterated overthis.values/v.valuesinstead ofx/y/z.I verified this against the 4D repro case in the issue (
[0,1,0,1],[1,0,1,0],t=0.5) and it produces the expected[0.707, 0.707, 0.707, 0.707].Degenerate case (theta close to PI): the existing code's fallback for "axis norm is 0 and angle is near PI" picks an arbitrary vector orthogonal to
this(using specific 3D constructions like(y, -x, 0)). In N dimensions there's no single obvious choice, antiparallel vectors leave an entire (N-1)-dimensional space of valid rotation planes, so any choice here is arbitrary, not a "true" generalization. I'll implement something reasonable (e.g., zero the largest-magnitude component and swap a pair of others to build an orthogonal vector), just wanted to flag this.Dimension mismatch:
dot()/mult()already truncate to the smaller vector's dimension viaprioritizeSmallerDimension. I'll haveslerp()follow the same convention for consistency, unless there's a preference otherwise.I'll update both the instance and static (
p5.Vector.slerp()) versions, and add 4D test cases to the existing slerp unit tests, including the near-antiparallel corner case from the thread above.