This method accurately replicates Vector3.SlerpUnclamed Code (csharp): // vectors need not be unit static public Vector3 SLerp(Vector3 a, Vector3 b, float t) { float magOf(Vector3 v) => MathF.Sqrt(Vector3.Dot(v, v)); float theta(Vector3 v0, Vector3 v1) => MathF.Atan2(magOf(Vector3.Cross(v0, v1)), Vector3.Dot(v0, v1)); var it = 1f - t; var m0 = magOf(a); var m1 = magOf(b); var v0 = a / m0; var v1 = b / m1; var th = theta(a, b); var rs = 1f / MathF.Sin(th); var j = MathF.Sin(it * th); var k = MathF.Sin(t * th); return (m0 * it + m1 * t) * (rs * j * v0 + rs * k * v1); } Of course, Slerp on its own isn't actually a mystery, but the trick was to make it work the same as Unity's. Because it works with non-unit vectors I had to experiment. In the end, the solution was to slerp the units, and then apply lerp of the original magnitudes. However, what's funky is that the theta is computed using the originals. Here, I've deliberately introduced magOf and theta methods to illustrate the amount of instructions needed. While Atan2 itself is very fast, we also need 3 Sqrt, 3 Sin, 7 dots (cross counts as 3), and 7 divisions. So I've tried to simplify this a bit. This is what I came up with. Code (csharp): // vectors need not be unit static public Vector3 SLerp(Vector3 a, Vector3 b, float t) { float magOf(Vector3 v) => MathF.Sqrt(Vector3.Dot(v, v)); float theta(Vector3 v0, Vector3 v1) => MathF.Atan2(magOf(Vector3.Cross(v0, v1)), Vector3.Dot(v0, v1)); var it = 1f - t; var m0 = magOf(a); var m1 = magOf(b); var th = theta(a, b); var s = MathF.Sin(th); var j = MathF.Sin(it * th); var k = MathF.Sin(t * th); return (m0 * it + m1 * t) * (j * m1 * a + k * m0 * b) / (s * m0 * m1); } 3 Sqrt, 7 dots, 1 Atan2, 3 Sin, 1 3 divisions (edit: oops it's still a vector scalar division, can't avoid that). I've managed to avoid pre-emptive normalization (because it got baked in the final division). However, if we work with unit vectors instead, things simplify A LOT. We no longer need to find magnitudes, and we also don't need a magnitude of a cross, for example, because it's now 1. Now we have Atan2(1, dot) which simplifies to Acos(dot). Finally, there's no need to lerp between the magnitudes anymore. Code (csharp): // vectors must be unit static public Vector3 SLerpUnit(Vector3 a, Vector3 b, float t) { var th = MathF.Acos(Vector3.Dot(a, b)); var s = MathF.Sin(th); var j = MathF.Sin((1f - t) * th); var k = MathF.Sin(t * th); return (j * a + k * b) / s; } This is significantly faster than the other one (1 Acos, 3 Sin, 1 dot, 1 3 divisions), however, each has its own use. An incredible thing with this unit Slerp is that it always runs on the sphere's great circle, guaranteeing the shortest path in the sphere's surface subspace. Unit vectors in that case, simply define two end points on such an arc. What if we could autodetect whether the two vectors were unit and pick the algorithm accordingly? This is what I did -- but it does introduce a slight overhead to both methods. Obviously not the brightest idea, but I left it in for posterity. Code (csharp): // autodetects unit vectors static public Vector3 SLerpAuto(Vector3 a, Vector3 b, float t) { bool isUnit(Vector3 v) => Math.Abs(Vector3.Dot(v, v) - 1f) <= 1E-4f; if(isUnit(a) && isUnit(b)) return SLerpUnit(a, b, t); return SLerp(a, b, t); } Here's the full component I used for testing. Spoiler Code (csharp): using System; using UnityEngine; public class NewSLerpTest : MonoBehaviour { [SerializeField] Vector3 point1; [SerializeField] Vector3 point2; [SerializeField] [Range(0, 10)] int innerPoints; [SerializeField] SLerpMethod mode = SLerpMethod.Unity; [SerializeField] bool forceUnit = false; [SerializeField] [Min(.001f)] float gizmoScale = 1f; private enum SLerpMethod { Unity = 1, Custom = 2, Both = 3 } void OnDrawGizmos() { var p1 = point1; var p2 = point2; if(forceUnit) { p1.Normalize(); p2.Normalize(); } drawPoint(p1, .1f, Color.white); drawPoint(p2, .1f, Color.yellow); for(int i = 0; i < innerPoints; i++) { float t = i / (float)innerPoints; Vector3 pt; if(((int)mode & 1) > 0) { pt = Vector3.SlerpUnclamped(p1, p2, t); drawPoint(pt, .06f, Color.cyan); } if(((int)mode & 2) > 0) { pt = SLerpAuto(p1, p2, t); drawPoint(pt, .06f, Color.magenta); } } } void drawPoint(Vector3 point, float radius = 1f, Color color = default) { if(color != default) Gizmos.color = color; Gizmos.DrawSphere(point, radius * gizmoScale); } // // vectors need not be unit (original unflattened) // static public Vector3 SLerp(Vector3 a, Vector3 b, float t) { // float magOf(Vector3 v) => MathF.Sqrt(Vector3.Dot(v, v)); // float theta(Vector3 v0, Vector3 v1) => MathF.Atan2(magOf(Vector3.Cross(v0, v1)), Vector3.Dot(v0, v1)); // var it = 1f - t; // var m0 = magOf(a); // var m1 = magOf(b); // var v0 = a / m0; // var v1 = b / m1; // var th = theta(a, b); // var rs = 1f / MathF.Sin(th); // var j = MathF.Sin(it * th); // var k = MathF.Sin(t * th); // return (m0 * it + m1 * t) * (rs * j * v0 + rs * k * v1); // } // vectors need not be unit static public Vector3 SLerp(Vector3 a, Vector3 b, float t) { float magOf(Vector3 v) => MathF.Sqrt(Vector3.Dot(v, v)); float theta(Vector3 v0, Vector3 v1) => MathF.Atan2(magOf(Vector3.Cross(v0, v1)), Vector3.Dot(v0, v1)); var it = 1f - t; var m0 = magOf(a); var m1 = magOf(b); var th = theta(a, b); var s = MathF.Sin(th); var j = MathF.Sin(it * th); var k = MathF.Sin(t * th); return (m0 * it + m1 * t) * (j * m1 * a + k * m0 * b) / (s * m0 * m1); } // vectors must be unit static public Vector3 SLerpUnit(Vector3 a, Vector3 b, float t) { var th = MathF.Acos(Vector3.Dot(a, b)); var s = MathF.Sin(th); var j = MathF.Sin((1f - t) * th); var k = MathF.Sin(t * th); return (j * a + k * b) / s; } // autodetects unit vectors static public Vector3 SLerpAuto(Vector3 a, Vector3 b, float t) { bool isUnit(Vector3 v) => Math.Abs(Vector3.Dot(v, v) - 1f) <= 1E-4f; if(isUnit(a) && isUnit(b)) return SLerpUnit(a, b, t); return SLerp(a, b, t); } } Haven't benchmarked any of this, performance-wise, because i'm lazy. I would be surprised if the fully-blown one matched the Unity's own SlerpUnclamped, but I believe the unit Slerp is faster (just an intuition without any evidence to back it up). Any feedback is appreciated.
I don't think there's any problem with me posting the C++ snippet for Unity implementation, just don't ask me or blame me for it, I didn't write it : Code (CSharp): Quaternionf Slerp(const Quaternionf& q1, const Quaternionf& q2, float t) { float dot = Dot(q1, q2); // dot = cos(theta) // if (dot < 0), q1 and q2 are more than 90 degrees apart, // so we can invert one to reduce spinning Quaternionf tmpQuat; if (dot < 0.0f) { dot = -dot; tmpQuat.Set(-q2.x, -q2.y, -q2.z, -q2.w); } else tmpQuat = q2; if (dot < 0.95f) { float angle = acos(dot); float sinadiv, sinat, sinaomt; sinadiv = 1.0f / sin(angle); sinat = sin(angle * t); sinaomt = sin(angle * (1.0f - t)); tmpQuat.Set((q1.x * sinaomt + tmpQuat.x * sinat) * sinadiv, (q1.y * sinaomt + tmpQuat.y * sinat) * sinadiv, (q1.z * sinaomt + tmpQuat.z * sinat) * sinadiv, (q1.w * sinaomt + tmpQuat.w * sinat) * sinadiv); return tmpQuat; } // if the angle is small, use linear interpolation else { return Lerp(q1, tmpQuat, t); } }
@MelvMay Thank you! For the record this is Quaternion.Slerp. There are some key differences, namely this is always unit Slerp.
I apologise, I'm not sure where my head was this morning! Here's the correct one: Code (CSharp): Vector3f Slerp(const Vector3f& lhs, const Vector3f& rhs, float t) { float lhsMag = Magnitude(lhs); float rhsMag = Magnitude(rhs); if (lhsMag < Vector3f::epsilon || rhsMag < Vector3f::epsilon) return Lerp(lhs, rhs, t); float lerpedMagnitude = Lerp(lhsMag, rhsMag, t); float dot = Dot(lhs, rhs) / (lhsMag * rhsMag); // direction is almost the same if (dot > 1.0F - Vector3f::epsilon) { return Lerp(lhs, rhs, t); } // directions are almost opposite else if (dot < -1.0F + Vector3f::epsilon) { Vector3f lhsNorm = lhs / lhsMag; Vector3f axis = OrthoNormalVectorFast(lhsNorm); Matrix3x3f m; m.SetAxisAngle(axis, kPI * t); Vector3f slerped = m.MultiplyPoint3(lhsNorm); slerped *= lerpedMagnitude; return slerped; } // normal case else { Vector3f axis = Cross(lhs, rhs); Vector3f lhsNorm = lhs / lhsMag; axis = Normalize(axis); float angle = std::acos(dot) * t; Matrix3x3f m; m.SetAxisAngle(axis, angle); Vector3f slerped = m.MultiplyPoint3(lhsNorm); slerped *= lerpedMagnitude; return slerped; } }
"Why don't you try to replicate Slerp?" they said "It'll be fun" they said lol Anyway, I managed to fix a lot of edge-case issues, including the antiparallel case. @MelvMay thanks a lot for the insight, that code was really useful. This is the final version, for the moment. I haven't been able to find a discrepancy for a while now, and this time I was hunting for all kinds of contrived situations ranging from rotation ambiguity to strict orthogonality, including cases like having point 1 at (10,10,10) and point 2 at (-1,-1,-1), for example. Earlier today this gave me a beautiful symmetric pattern but alas I had to hate it because it was so wrong. Code (csharp): // vectors need not be unit static public Vector3 SLerp(Vector3 a, Vector3 b, float t) { float magOf(Vector3 v) => MathF.Sqrt(Vector3.Dot(v, v)); var it = 1f - t; // inverse of t var m0 = magOf(a); var m1 = magOf(b); var mm = m0 * m1; // combined magnitude if(mm == 0f) return a * it + b * t; // use lerp if one of the vectors is zero var d = Vector3.Dot(a, b) / mm; // unit dot if(1f - Math.Abs(d) <= 1E-5f) { // abs(dot) is close to 1 return d > 0f? a * it + b * t // << use lerp for very small angles : AxisAngle(FastOrthogonal(a), MathF.PI * t) * ((it + (m1/m0) * t) * a); } // ^^ vectors are antiparallel, apply rotation on orthogonal axis, lerp mag var th = MathF.Acos(d); var s = MathF.Sin(th) * mm; var j = MathF.Sin(it * th); var k = MathF.Sin(t * th); // left-hand-side scalar part = mag lerp // right-hand-side vector part = actual slerp return (m0 * it + m1 * t) * (j * m1 * a + k * m0 * b) / s; } Anyway it turned out that I needed a much more robust orthonormal function, so I finally got to make one. Code (csharp): static public Vector3 FastOrthogonal(Vector3 v, bool normalize = true) { var sqr = v.x * v.x + v.y * v.y; if(sqr > 0f) { // (0,0,1) x (x,y,z) var im = normalize? 1f / MathF.Sqrt(sqr) : 1f; return new Vector3(-v.y * im, v.x * im, 0f); } else { // (1,0,0) x (x,y,z) sqr = v.y * v.y + v.z * v.z; var im = normalize? 1f / MathF.Sqrt(sqr) : 1f; return new Vector3(0f, -v.z * im, v.y * im); } } This is effectively a hacked cross product, which optionally normalizes the result (aka returns orthonormal). We can also do FastOrthogonal(a/m0, false) but in any case axis has to be unit, because quaternion expects a normal. Also the antiparallel solution is a weird one, and here I'm hijacking quaternions for the final multiplication, but I bypass normalization (and a conversion from degrees to radians). AxisAngle is thus Code (csharp): // axis must be unit, rad is angle in radians static public Quaternion AxisAngle(Vector3 axis, float rad) { rad *= .5f; axis *= MathF.Sin(rad); var q = new Quaternion(axis.x, axis.y, axis.z, MathF.Cos(rad)); //Debug.Assert(q == q.normalized); return q; } I couldn't notice any scenario in which this quaternion wasn't unit (if the axis was unit), so I skipped that part. I will probably stress-test this some more and analyze the math in more detail. I'm too lightheaded these days for any kind of complex algebra. Finally, because I rotate vector a directly, mag lerp is a bit different than usual, basically I just scale it from 1 towards b/a. Hence Code (csharp): AxisAngle(FastOrthogonal(a), MathF.PI * t) * ((it + (m1/m0) * t) * a); A much simpler SLerpUnit now looks like this Code (csharp): // vectors must be unit static public Vector3 SLerpUnit(Vector3 a, Vector3 b, float t) { var d = Vector3.Dot(a, b); if(1f - Math.Abs(d) <= 1E-6f) // smaller epsilon return d > 0f? a * (1f - t) + b * t : AxisAngle(FastOrthogonal(a, normalize: false), MathF.PI * t) * a; var th = MathF.Acos(d); var s = MathF.Sin(th); var j = MathF.Sin((1f - t) * th); var k = MathF.Sin(t * th); return (j * a + k * b) / s; }