Skip to content

Commit

Permalink
add slerpQuaternions()
Browse files Browse the repository at this point in the history
  • Loading branch information
jespertheend committed Aug 12, 2024
1 parent 52ee7c5 commit f26d4a5
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 1 deletion.
43 changes: 43 additions & 0 deletions src/math/Quat.js
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,49 @@ export class Quat {
return this;
}

/**
* Interpolates between `quatA` and `quatB` without modifying and returns a quaternion with the result.
* @param {Quat} quatA
* @param {Quat} quatB
* @param {number} t
*/
static slerpQuaternions(quatA, quatB, t) {
// https://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/slerp/
if (t == 0) {
return quatA.clone();
}
if (t == 1) {
return quatB.clone();
}

let cosHalfTheta = new Vec4(quatA).dot(quatB);
if (cosHalfTheta < 0) {
quatB.x = -quatB.x;
quatB.y = -quatB.y;
quatB.w = -quatB.w;
cosHalfTheta = -cosHalfTheta;
}

// If quatA = quatB or quatA = -quatB then theta = 0 and we can return quatA
if (Math.abs(cosHalfTheta) >= 1) {
return quatA.clone();
}

// Calculate temporary values.
const halfTheta = Math.acos(cosHalfTheta);
const sinHalfTheta = Math.sqrt(1 - cosHalfTheta * cosHalfTheta);

const ratioA = Math.sin((1 - t) * halfTheta) / sinHalfTheta;
const ratioB = Math.sin(t * halfTheta) / sinHalfTheta;

return new Quat(
quatA.x * ratioA + quatB.x * ratioB,
quatA.y * ratioA + quatB.y * ratioB,
quatA.z * ratioA + quatB.z * ratioB,
quatA.w * ratioA + quatB.w * ratioB,
);
}

/**
* @returns {[x: number, y: number, z: number, w: number]}
*/
Expand Down
59 changes: 58 additions & 1 deletion test/unit/src/math/Quat.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { assertEquals } from "std/testing/asserts.ts";
import { Quat, Vec3 } from "../../../../src/mod.js";
import { assertVecAlmostEquals } from "../../../../src/util/asserts.js";
import { assertQuatAlmostEquals, assertVecAlmostEquals } from "../../../../src/util/asserts.js";

Deno.test({
name: "rotateAxisAngle()",
Expand All @@ -26,3 +26,60 @@ Deno.test({
assertEquals(result, "Quat<1, 2, 3, 4>");
},
});

/**
* @param {Quat} a
* @param {Quat} b
* @param {number} t
* @param {Quat} expected
*/
function basicSlerpTest(a, b, t, expected) {
const result = Quat.slerpQuaternions(a, b, t);
assertQuatAlmostEquals(result, expected);
}

Deno.test({
name: "slerp two identity quaternions",
fn() {
basicSlerpTest(new Quat(), new Quat(), 0, new Quat());
basicSlerpTest(new Quat(), new Quat(), 0.123, new Quat());
basicSlerpTest(new Quat(), new Quat(), 0.2, new Quat());
basicSlerpTest(new Quat(), new Quat(), 0.5, new Quat());
basicSlerpTest(new Quat(), new Quat(), 1, new Quat());
},
});

Deno.test({
name: "basic 180 degree slerp",
fn() {
const a = new Quat();
const b = Quat.fromAxisAngle(0, 1, 0, Math.PI);
basicSlerpTest(a, b, 0, a);
basicSlerpTest(a, b, 0.1, Quat.fromAxisAngle(0, 1, 0, Math.PI * 0.1));
basicSlerpTest(a, b, 0.25, Quat.fromAxisAngle(0, 1, 0, Math.PI * 0.25));
basicSlerpTest(a, b, 0.5, Quat.fromAxisAngle(0, 1, 0, Math.PI * 0.5));
basicSlerpTest(a, b, 0.75, Quat.fromAxisAngle(0, 1, 0, Math.PI * 0.75));
basicSlerpTest(a, b, 0.9, Quat.fromAxisAngle(0, 1, 0, Math.PI * 0.9));
basicSlerpTest(a, b, 1, b);
},
});

Deno.test({
name: "slerp that results in a negative cosHalfTheta",
fn() {
const a = Quat.fromAxisAngle(0, 1, 0, 2);
const b = Quat.fromAxisAngle(0, 1, 0, -2);
basicSlerpTest(a, b, 0.219, Quat.fromAxisAngle(0, 1, 0, 2.5));
basicSlerpTest(a, b, 0.5, Quat.fromAxisAngle(0, 1, 0, Math.PI));
},
});

Deno.test({
name: "slerp two quaternions that are the same",
fn() {
const a = new Quat(0, 0.2, 20, 1);
basicSlerpTest(a, a, 0.5, a);
const b = new Quat(12, 34, 56, 78);
basicSlerpTest(b, b, 0.5, b);
},
});

0 comments on commit f26d4a5

Please sign in to comment.