Skip to content

Commit 89a6788

Browse files
authored
fix(math): Fix slerpQuaternions results being wrong at certain angles (#961)
1 parent 951efa1 commit 89a6788

File tree

2 files changed

+51
-12
lines changed

2 files changed

+51
-12
lines changed

src/math/Quat.js

+15-9
Original file line numberDiff line numberDiff line change
@@ -285,31 +285,37 @@ export class Quat {
285285
* @param {number} t
286286
*/
287287
static slerpQuaternions(quatA, quatB, t) {
288+
quatA = quatA.clone();
289+
quatB = quatB.clone();
288290
// https://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/slerp/
289-
if (t == 0) {
290-
return quatA.clone();
291-
}
292-
if (t == 1) {
293-
return quatB.clone();
294-
}
291+
if (t == 0) return quatA;
292+
if (t == 1) return quatB;
295293

296294
let cosHalfTheta = new Vec4(quatA).dot(quatB);
297295
if (cosHalfTheta < 0) {
298296
quatB.x = -quatB.x;
299297
quatB.y = -quatB.y;
298+
quatB.z = -quatB.z;
300299
quatB.w = -quatB.w;
301300
cosHalfTheta = -cosHalfTheta;
302301
}
303302

304303
// If quatA = quatB or quatA = -quatB then theta = 0 and we can return quatA
305-
if (Math.abs(cosHalfTheta) >= 1) {
304+
if (cosHalfTheta >= 1) {
306305
return quatA.clone();
307306
}
308307

309-
// Calculate temporary values.
310308
const halfTheta = Math.acos(cosHalfTheta);
311-
const sinHalfTheta = Math.sqrt(1 - cosHalfTheta * cosHalfTheta);
309+
const sqrSinHalfTheta = 1 - cosHalfTheta * cosHalfTheta;
310+
311+
// When sinHalfTheta is very small, we run into precision errors.
312+
// It's best to just linearly interpolate the two quaternions in that case
313+
if (sqrSinHalfTheta <= Number.EPSILON) {
314+
const vec = Vec4.lerp(quatA, quatB, t);
315+
return new Quat(vec);
316+
}
312317

318+
const sinHalfTheta = Math.sqrt(sqrSinHalfTheta);
313319
const ratioA = Math.sin((1 - t) * halfTheta) / sinHalfTheta;
314320
const ratioB = Math.sin(t * halfTheta) / sinHalfTheta;
315321

test/unit/src/math/Quat.test.js

+36-3
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ Deno.test({
3333
* @param {number} t
3434
* @param {Quat} expected
3535
*/
36-
function basicSlerpTest(a, b, t, expected) {
36+
function basicSlerpTest(a, b, t, expected, tolerance = 0.00001) {
3737
const result = Quat.slerpQuaternions(a, b, t);
38-
assertQuatAlmostEquals(result, expected);
38+
assertQuatAlmostEquals(result, expected, tolerance);
3939
}
4040

4141
Deno.test({
@@ -64,6 +64,21 @@ Deno.test({
6464
},
6565
});
6666

67+
Deno.test({
68+
name: "two quaternions that are very close to each other",
69+
fn() {
70+
const a = new Quat();
71+
const b = Quat.fromAxisAngle(0, 1, 0, 0.00000003);
72+
basicSlerpTest(a, b, 0, a, 0);
73+
basicSlerpTest(a, b, 0.1, new Quat(0, 1.5e-9, 0, 1), 0);
74+
basicSlerpTest(a, b, 0.25, new Quat(0, 3.75e-9, 0, 1), 0);
75+
basicSlerpTest(a, b, 0.5, new Quat(0, 7.5e-9, 0, 1), 0);
76+
basicSlerpTest(a, b, 0.75, new Quat(0, 1.1249999999999998e-8, 0, 0.9999999999999999), 0);
77+
basicSlerpTest(a, b, 0.9, new Quat(0, 1.3499999999999998e-8, 0, 0.9999999999999999), 0);
78+
basicSlerpTest(a, b, 1, b, 0);
79+
},
80+
});
81+
6782
Deno.test({
6883
name: "slerp that results in a negative cosHalfTheta",
6984
fn() {
@@ -74,12 +89,30 @@ Deno.test({
7489
},
7590
});
7691

92+
Deno.test({
93+
name: "another slerp that results in a negative cosHalfTheta",
94+
fn() {
95+
const a = new Quat();
96+
const b = Quat.fromAxisAngle(1, 1, 1, 4);
97+
basicSlerpTest(a, b, 0.219, Quat.fromAxisAngle(1, 1, 1, -0.5));
98+
basicSlerpTest(a, b, 0.5, Quat.fromAxisAngle(1, 1, 1, -1.14157));
99+
},
100+
});
101+
77102
Deno.test({
78103
name: "slerp two quaternions that are the same",
79104
fn() {
80105
const a = new Quat(0, 0.2, 20, 1);
81-
basicSlerpTest(a, a, 0.5, a);
82106
const b = new Quat(12, 34, 56, 78);
107+
basicSlerpTest(a, a, 0.01, a);
108+
basicSlerpTest(b, b, 0.01, b);
109+
basicSlerpTest(a, a, 0.1, a);
110+
basicSlerpTest(b, b, 0.1, b);
111+
basicSlerpTest(a, a, 0.5, a);
83112
basicSlerpTest(b, b, 0.5, b);
113+
basicSlerpTest(a, a, 0.9, a);
114+
basicSlerpTest(b, b, 0.9, b);
115+
basicSlerpTest(a, a, 0.99, a);
116+
basicSlerpTest(b, b, 0.99, b);
84117
},
85118
});

0 commit comments

Comments
 (0)