So in an effort to save some confusion from others who may go through this I knocked together some SDL/OpenGL code in an effort to get either Mauricio's or Martin's answer to work. I found Martin's answer as it stands a little bit nebulous when implementing even though it states the truth. I haven't managed to get Mauricio's answer to work even with his help unfortunately.
I made a few mistakes too, and the number of different slerp functions I tried from various places to sanity check things ended up causing me some confusion so I ended up implementing my own slerp (SlerpIam() in the code) from scratch without checking for the nearest path.
In the code Slerp1() & Slerp2() I think are broken for when the shortest path is not selected which was part of my confusion - as from the myriad of slerps I found I think they were erroneously modified to try and support longest paths but they don't. So I was originally trying to hack them as Martin mentioned but it went horribly wrong.
My test case shows a point being rotated/rolled around the Z axis to 270 degrees.
I compiled the code with SDL2 on Windows and you will need to include SDL headers and link etc:
#include <cmath>
constexpr float PI = 3.14159265358979323846;
struct Quat { float x, y, z, w; };
struct Vec3 { float x, y, z; };
struct AxisAngle { Vec3 axis; float angle; };
float ToRadian(float degree) { return degree * PI / 180.0f; }
Quat operator+(Quat a, Quat b) { return { a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w }; }
Quat operator*(Quat q, float s) { return { q.x * s, q.y * s, q.z * s, q.w * s }; }
Quat operator*(float s, Quat q) { return { q.x * s, q.y * s, q.z * s, q.w * s }; }
Quat operator*(Quat second, Quat first)
{
return Quat
{
second.w*first.x + second.x*first.w + second.y*first.z - second.z*first.y,
second.w*first.y - second.x*first.z + second.y*first.w + second.z*first.x,
second.w*first.z + second.x*first.y - second.y*first.x + second.z*first.w,
second.w*first.w - second.x*first.x - second.y*first.y - second.z*first.z
};
}
float Dot(Quat a, Quat b) { return a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w; }
float Length(Quat q) { return sqrtf(Dot(q, q)); }
Quat Normalise(Quat q) { return q * (1.0f / sqrtf(Dot(q, q))); }
Quat Conjugate(Quat q) { return{ -q.x, -q.y, -q.z, q.w }; }
Quat Reciprocal(Quat q) { return Conjugate(q) * (1.0f / Dot(q, q)); }
Vec3 Rotate(Quat q, Vec3 v) { Quat vr = q * Quat{ v.x, v.y, v.z, 0.0f } *Conjugate(q); return { vr.x, vr.y, vr.z }; }
Quat ToQuat(AxisAngle r)
{
float halfAngle = 0.5f * r.angle;
float sinHalfAngle = sinf(halfAngle);
return{ r.axis.x * sinHalfAngle, r.axis.y * sinHalfAngle, r.axis.z * sinHalfAngle, cosf(halfAngle) };
}
AxisAngle ToAxisAngle(Quat q)
{
float s = 1.0f / sqrtf(1.0f - q.w * q.w);
return { { q.x * s, q.y * s, q.z * s }, acosf(q.w) * 2.0f };
}
Quat Exp(Quat q)
{
double b = sqrt(q.x * q.x + q.y * q.y + q.z * q.z);
if (fabs(b) <= 1.0e-14 * fabs(q.w))
return { 0.0f, 0.0f, 0.0f, expf(q.w) };
else
{
float e = expf(q.w);
float f = sinf(b) / b;
return { e * f * q.x, e * f * q.y, e * f * q.z, e * cosf(b) };
}
}
Quat SlerpIam(Quat a, Quat b, float t)
{
float dotAB = Dot(a, b);
float theta = acosf(dotAB);
float sinTheta = sinf(theta);
float af = sinf((1.0f - t) * theta) / sinTheta;
float bf = sinf(t * theta) / sinTheta;
return a * af + b * bf;
}
Quat Slerp1(Quat q0, Quat q1, float t, bool shortPath = true)
{
float d = Dot(q0, q1);
float s0, s1;
float sd = shortPath ? (d > 0) - (d < 0) : 1.0f;
d = fabs(d);
if (d < 0.9995f)
{
float s = sqrtf(1 - d * d); // Sine of relative angle
float a = atan2f(s, d);
float c = cosf(t*a);
s1 = sqrtf(1 - c * c) / s;
s0 = c - d * s1;
}
else
{
s0 = 1.0f - t;
s1 = t;
}
return q0 * s0 + q1 * sd * s1;
}
Quat Slerp2(Quat q0, Quat q1, float t, bool shortPath = true)
{
float a = 1.0f - t;
float b = t;
float d = Dot(q0, q1);
float c = fabsf(d);
if (c < 0.9995f)
{
c = acosf(c);
b = 1.0f / sinf(c);
a = sinf(a * c) * b;
b *= sinf(t * c);
if (shortPath && d < 0)
b = -b;
}
return q0 * a + q1 * b;
}
Quat FarSlerpMauricio(Quat q0, Quat q1, float t)
{
Quat q01 = Reciprocal(q0) * q1;
Quat Vdash{ q01.x, q01.y, q01.z, 0.0f };
Quat V = Vdash * (1.0f / Length(Vdash));
float theta = 2.0f * atan2f(Length(Vdash), q01.w);
float CompTheta = theta - 2.0f * M_PI;
return q1 * Exp(t * CompTheta * V * 0.5f);
}
void Draw()
{
float t = float(SDL_GetTicks() % 6000) / 6000.0f;
Quat id{ 0.0f, 0.0f, 0.0f, 1.0f};
Quat target = ToQuat({{0.0f, 0.0f, 1.0f}, ToRadian(270.0f)});
//Quat r = FarSlerpMauricio(id, target, t);
Quat r = SlerpIam(id, target, t);
//Quat r = Slerp1(id, target, t);
//Quat r = Slerp2(id, target, t);
Vec3 p = Rotate(r, { 1.0f, 0.0f, 0.0f });
glClearColor(0.2f, 0.2f, 0.2f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glBegin(GL_LINES);
// Floor grid
glColor3f(0.13f, 0.13f, 0.13f);
for (int i = 0; i < 8; ++i)
{
float f = 2.0f * float(i) / 7.0f - 1.0f;
glVertex3f(-1.0f, 0.0f, f);
glVertex3f(+1.0f, 0.0f, f);
glVertex3f(f, 0.0f, -1.0f);
glVertex3f(f, 0.0f, +1.0f);
}
// Axii
glColor3f(0.8f, 0.0f, 0.0f);
glVertex3f(0.0f, 0.0f, 0.0f);
glVertex3f(1.0f, 0.0f, 0.0f);
glColor3f(0.0f, 0.8f, 0.0f);
glVertex3f(0.0f, 0.0f, 0.0f);
glVertex3f(0.0f, 1.0f, 0.0f);
glColor3f(0.0f, 0.0f, 0.8f);
glVertex3f(0.0f, 0.0f, 0.0f);
glVertex3f(0.0f, 0.0f, 1.0f);
// Ray to path
glColor3f(1.0f, 1.0f, 1.0f);
glVertex3f(0.0f, 0.0f, 0.0f);
glVertex3fv(&p.x);
glEnd();
}
int main()
{
SDL_GLContext openGL;
SDL_Window* window;
bool run = true;
if (SDL_Init(SDL_INIT_VIDEO) < 0)
return -1;
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1);
SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 8);
if (!(window = SDL_CreateWindow("slerp", 100, 100, 800, 800, SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN)))
return -1;
openGL = SDL_GL_CreateContext(window);
glViewport(0, 0, 800, 800);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(-2.0f, 2.0f, -2.0f, 2.0f, -2.0f, 2.0f);
glRotatef(45.0f, 1.0f, 0.0f, 0.0f);
glRotatef(45.0f, 0.0f, 1.0f, 0.0f);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
glClearDepth(1.0f);
glDisable(GL_CULL_FACE);
glCullFace(GL_BACK);
glFrontFace(GL_CCW);
while (run)
{
SDL_Event event;
while (SDL_PollEvent(&event) != 0)
{
if (event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEMOTION)
;
if (event.type == SDL_QUIT)
run = false;
}
Draw();
SDL_GL_SwapWindow(window);
}
SDL_GL_DeleteContext(openGL);
SDL_DestroyWindow(window);
return 0;
}
So working from my own SlerpIam() from scratch, I think my sanity is restored and Martin's answer is in essence correct. I get the following functions that I think are correct (Note they don't deal with small angle lerp fallback presently):
Quat SlerpNear(Quat a, Quat b, float t)
{
float dotAB = Dot(a, b);
if (dotAB < 0.0f)
{
dotAB = -dotAB;
b = -b;
}
float theta = acosf(dotAB);
float sinTheta = sinf(theta);
float af = sinf((1.0f - t) * theta) / sinTheta;
float bf = sinf(t * theta) / sinTheta;
return a * af + b * bf;
}
Quat SlerpFar(Quat a, Quat b, float t)
{
float dotAB = Dot(a, b);
if (dotAB > 0.0f)
{
dotAB = -dotAB;
b = -b;
}
float theta = acosf(dotAB);
float sinTheta = sinf(theta);
float af = sinf((1.0f - t) * theta) / sinTheta;
float bf = sinf(t * theta) / sinTheta;
return a * af + b * bf;
}