Line data Source code
1 : #include "catch.hpp"
2 : #include "Quaternion.h"
3 : #include "Vector.h"
4 : #include <cmath>
5 :
6 23 : SCENARIO( "Quaternion basics", "[Quaternion]" ) {
7 23 : GIVEN( "Default quaternion" ) {
8 1 : Quaternion q;
9 2 : THEN( "all components are zero" ) {
10 1 : REQUIRE(q.getX() == 0.0f);
11 1 : REQUIRE(q.getY() == 0.0f);
12 1 : REQUIRE(q.getZ() == 0.0f);
13 1 : REQUIRE(q.getW() == 0.0f);
14 1 : }
15 1 : }
16 :
17 23 : GIVEN( "loadMultIdentity" ) {
18 1 : Quaternion q(1.0f, 2.0f, 3.0f, 4.0f);
19 1 : q.loadMultIdentity();
20 2 : THEN( "is multiplicative identity" ) {
21 1 : REQUIRE(q.getX() == 0.0f);
22 1 : REQUIRE(q.getY() == 0.0f);
23 1 : REQUIRE(q.getZ() == 0.0f);
24 1 : REQUIRE(q.getW() == 1.0f);
25 1 : }
26 1 : }
27 :
28 23 : GIVEN( "loadAddIdentity" ) {
29 1 : Quaternion q(1.0f, 2.0f, 3.0f, 4.0f);
30 1 : q.loadAddIdentity();
31 2 : THEN( "is additive zero quaternion" ) {
32 1 : REQUIRE(q.getX() == 0.0f);
33 1 : REQUIRE(q.getY() == 0.0f);
34 1 : REQUIRE(q.getZ() == 0.0f);
35 1 : REQUIRE(q.getW() == 0.0f);
36 1 : }
37 1 : }
38 :
39 23 : GIVEN( "Quaternion from explicit components" ) {
40 1 : Quaternion q(0.0f, 0.0f, 0.0f, 2.0f);
41 1 : q.normalize();
42 2 : THEN( "normalize yields unit w" ) {
43 1 : REQUIRE(q.getW() == Approx(1.0f));
44 1 : REQUIRE(q.getX() == Approx(0.0f));
45 1 : REQUIRE(q.getY() == Approx(0.0f));
46 1 : REQUIRE(q.getZ() == Approx(0.0f));
47 1 : }
48 1 : }
49 :
50 23 : GIVEN( "normalize of a zero-length quaternion" ) {
51 1 : Quaternion q; // default: all components zero, len == 0
52 1 : q.normalize();
53 2 : THEN( "all components remain zero — no-op guard prevents divide by zero" ) {
54 1 : REQUIRE(q.getX() == 0.0f);
55 1 : REQUIRE(q.getY() == 0.0f);
56 1 : REQUIRE(q.getZ() == 0.0f);
57 1 : REQUIRE(q.getW() == 0.0f);
58 1 : }
59 1 : }
60 :
61 23 : GIVEN( "float constructor delegates to buildFromEuler" ) {
62 1 : Quaternion fromCtor(0.0f, 1.0f, 0.0f); // Quaternion(float, float, float)
63 1 : Quaternion fromMethod;
64 1 : fromMethod.buildFromEuler(0.0f, 1.0f, 0.0f);
65 2 : THEN( "constructor and explicit buildFromEuler produce identical results" ) {
66 1 : REQUIRE(fromCtor.getX() == Approx(fromMethod.getX()));
67 1 : REQUIRE(fromCtor.getY() == Approx(fromMethod.getY()));
68 1 : REQUIRE(fromCtor.getZ() == Approx(fromMethod.getZ()));
69 1 : REQUIRE(fromCtor.getW() == Approx(fromMethod.getW()));
70 1 : }
71 1 : }
72 :
73 23 : GIVEN( "Vector constructor delegates to buildFromEuler" ) {
74 1 : Vector v(0.0f, 1.0f, 0.0f);
75 1 : Quaternion fromCtor(v); // Quaternion(const Vector&)
76 1 : Quaternion fromMethod;
77 1 : fromMethod.buildFromEuler(0.0f, 1.0f, 0.0f);
78 2 : THEN( "constructor and explicit buildFromEuler produce identical results" ) {
79 1 : REQUIRE(fromCtor.getX() == Approx(fromMethod.getX()));
80 1 : REQUIRE(fromCtor.getY() == Approx(fromMethod.getY()));
81 1 : REQUIRE(fromCtor.getZ() == Approx(fromMethod.getZ()));
82 1 : REQUIRE(fromCtor.getW() == Approx(fromMethod.getW()));
83 1 : }
84 1 : }
85 :
86 23 : GIVEN( "buildFromEuler with zero angles" ) {
87 1 : Quaternion q;
88 1 : q.buildFromEuler(0.0f, 0.0f, 0.0f);
89 2 : THEN( "matches identity rotation" ) {
90 1 : REQUIRE(q.getW() == Approx(1.0f));
91 1 : REQUIRE(q.getX() == Approx(0.0f));
92 1 : REQUIRE(q.getY() == Approx(0.0f));
93 1 : REQUIRE(q.getZ() == Approx(0.0f));
94 1 : }
95 1 : }
96 :
97 23 : GIVEN( "buildRotationMatrix from multiplicative identity" ) {
98 1 : Quaternion q;
99 1 : q.loadMultIdentity();
100 1 : Matrix m;
101 1 : q.buildRotationMatrix(m);
102 :
103 2 : THEN( "result is the identity matrix" ) {
104 1 : REQUIRE(m.data[0] == Approx(1.0f));
105 1 : REQUIRE(m.data[5] == Approx(1.0f));
106 1 : REQUIRE(m.data[10] == Approx(1.0f));
107 1 : REQUIRE(m.data[15] == Approx(1.0f));
108 1 : REQUIRE(m.data[1] == Approx(0.0f));
109 1 : REQUIRE(m.data[2] == Approx(0.0f));
110 1 : REQUIRE(m.data[4] == Approx(0.0f));
111 1 : REQUIRE(m.data[6] == Approx(0.0f));
112 1 : REQUIRE(m.data[9] == Approx(0.0f));
113 1 : }
114 1 : }
115 :
116 24 : GIVEN( "buildFromAxis on X by 90 degrees" ) {
117 2 : Vector axis(1.0f, 0.0f, 0.0f);
118 2 : const float halfPi = 1.57079632679f;
119 2 : Quaternion q(axis, halfPi);
120 2 : q.normalize();
121 2 : Matrix m;
122 2 : q.buildRotationMatrix(m);
123 2 : Vector euler;
124 2 : q.getEulerAngles(euler);
125 :
126 3 : THEN( "rotation matrix for 90 deg about X maps Y to Z" ) {
127 1 : REQUIRE(m.data[0] == Approx(1.0f));
128 1 : REQUIRE(m.data[5] == Approx(0.0f));
129 1 : REQUIRE(m.data[9] == Approx(-1.0f));
130 1 : REQUIRE(m.data[6] == Approx(1.0f));
131 1 : }
132 :
133 3 : THEN( "euler extraction returns finite angles" ) {
134 1 : REQUIRE(std::isfinite(euler.x));
135 1 : REQUIRE(std::isfinite(euler.y));
136 1 : REQUIRE(std::isfinite(euler.z));
137 1 : }
138 2 : }
139 :
140 23 : GIVEN( "getEulerAngles at 90 degrees about Y (gimbal lock)" ) {
141 : // 90 deg about Y produces m.data[2] = -1, which is outside the epsilon
142 : // band and enters the gimbal-lock else branch. v.z is forced to 0;
143 : // v.y and v.x are computed from atan2 with exact inputs.
144 1 : const float halfPi = 1.57079632679f;
145 1 : Quaternion q(Vector(0.0f, 1.0f, 0.0f), halfPi);
146 1 : q.normalize();
147 1 : Vector euler;
148 1 : q.getEulerAngles(euler);
149 :
150 2 : THEN( "euler matches analytic gimbal-lock result (0, pi/2, 0)" ) {
151 1 : REQUIRE(euler.x == Approx(0.0f));
152 1 : REQUIRE(euler.y == Approx(halfPi));
153 1 : REQUIRE(euler.z == Approx(0.0f));
154 1 : }
155 1 : }
156 :
157 23 : GIVEN( "Copy constructor and assignment" ) {
158 1 : Quaternion a(1.0f, 2.0f, 3.0f, 4.0f);
159 1 : Quaternion b(a);
160 1 : Quaternion c;
161 1 : c = a;
162 :
163 2 : THEN( "copies match source" ) {
164 1 : REQUIRE(b.getX() == a.getX());
165 1 : REQUIRE(c.getW() == a.getW());
166 1 : }
167 1 : }
168 :
169 23 : GIVEN( "Quaternion multiplication composes rotations" ) {
170 1 : Quaternion q1;
171 1 : q1.buildFromEuler(0.0f, 0.0f, 0.0f);
172 1 : Quaternion q2;
173 1 : q2.loadMultIdentity();
174 1 : Quaternion r = q1 * q2;
175 2 : THEN( "identity times identity" ) {
176 1 : REQUIRE(r.getW() == Approx(1.0f));
177 1 : REQUIRE(r.getX() == Approx(0.0f));
178 1 : }
179 1 : }
180 :
181 23 : GIVEN( "90 degrees about X composed with 90 degrees about Y" ) {
182 1 : const float halfPi = 1.57079632679f;
183 1 : Quaternion q1(Vector(1.0f, 0.0f, 0.0f), halfPi);
184 1 : Quaternion q2(Vector(0.0f, 1.0f, 0.0f), halfPi);
185 1 : Quaternion r = q1 * q2;
186 :
187 2 : THEN( "result matches analytic composition (0.5, 0.5, -0.5, 0.5)" ) {
188 1 : REQUIRE(r.getX() == Approx(0.5f));
189 1 : REQUIRE(r.getY() == Approx(0.5f));
190 1 : REQUIRE(r.getZ() == Approx(-0.5f));
191 1 : REQUIRE(r.getW() == Approx(0.5f));
192 1 : }
193 1 : }
194 :
195 23 : GIVEN( "operator* is non-commutative" ) {
196 : // q1*q2 = (0.5, 0.5, -0.5, 0.5); q2*q1 = (0.5, 0.5, 0.5, 0.5).
197 : // The results differ in z, confirming multiplication order matters.
198 1 : const float halfPi = 1.57079632679f;
199 1 : Quaternion q1(Vector(1.0f, 0.0f, 0.0f), halfPi); // 90 deg about X
200 1 : Quaternion q2(Vector(0.0f, 1.0f, 0.0f), halfPi); // 90 deg about Y
201 :
202 1 : Quaternion r12 = q1 * q2;
203 1 : Quaternion r21 = q2 * q1;
204 :
205 2 : THEN( "q1*q2 and q2*q1 differ" ) {
206 1 : REQUIRE(r12.getZ() == Approx(-0.5f));
207 1 : REQUIRE(r21.getZ() == Approx( 0.5f));
208 1 : }
209 1 : }
210 :
211 23 : GIVEN( "operator+ adds components" ) {
212 1 : Quaternion a(1.0f, 2.0f, 3.0f, 4.0f);
213 1 : Quaternion b(10.0f, 20.0f, 30.0f, 40.0f);
214 1 : Quaternion c = a + b;
215 2 : THEN( "sum is per-component" ) {
216 1 : REQUIRE(c.getX() == Approx(11.0f));
217 1 : REQUIRE(c.getY() == Approx(22.0f));
218 1 : REQUIRE(c.getZ() == Approx(33.0f));
219 1 : REQUIRE(c.getW() == Approx(44.0f));
220 1 : }
221 1 : }
222 :
223 23 : GIVEN( "slerp with t < 0" ) {
224 1 : Quaternion qStart;
225 1 : qStart.loadMultIdentity();
226 1 : Quaternion qEnd;
227 1 : qEnd.buildFromEuler(0.0f, 1.0f, 0.0f);
228 1 : qEnd.normalize();
229 :
230 1 : Quaternion result; // all-zero default
231 1 : result.slerp(qStart, -0.5f, qEnd);
232 :
233 2 : THEN( "result is unchanged — slerp is a no-op for out-of-range t" ) {
234 1 : REQUIRE(result.getX() == 0.0f);
235 1 : REQUIRE(result.getY() == 0.0f);
236 1 : REQUIRE(result.getZ() == 0.0f);
237 1 : REQUIRE(result.getW() == 0.0f);
238 1 : }
239 1 : }
240 :
241 23 : GIVEN( "slerp with t > 1" ) {
242 1 : Quaternion qStart;
243 1 : qStart.loadMultIdentity();
244 1 : Quaternion qEnd;
245 1 : qEnd.buildFromEuler(0.0f, 1.0f, 0.0f);
246 1 : qEnd.normalize();
247 :
248 1 : Quaternion result; // all-zero default
249 1 : result.slerp(qStart, 2.0f, qEnd);
250 :
251 2 : THEN( "result is unchanged — slerp is a no-op for out-of-range t" ) {
252 1 : REQUIRE(result.getX() == 0.0f);
253 1 : REQUIRE(result.getY() == 0.0f);
254 1 : REQUIRE(result.getZ() == 0.0f);
255 1 : REQUIRE(result.getW() == 0.0f);
256 1 : }
257 1 : }
258 :
259 23 : GIVEN( "slerp with negated end quaternion (negative dot product branch)" ) {
260 : // Negating all components of a quaternion yields the same rotation.
261 : // When dotQ < 0, slerp flips eQ to take the short arc. At t=1 the
262 : // result should still land on the positive form of the end rotation.
263 1 : const float halfPi = 1.57079632679f;
264 1 : const float s = std::sin(halfPi / 2.0f); // sin(pi/4) = sqrt(2)/2
265 1 : const float c = std::cos(halfPi / 2.0f); // cos(pi/4) = sqrt(2)/2
266 :
267 1 : Quaternion qStart;
268 1 : qStart.loadMultIdentity();
269 1 : Quaternion qEndNeg(0.0f, -s, 0.0f, -c); // negated 90 deg about Y
270 :
271 1 : Quaternion result;
272 1 : result.slerp(qStart, 1.0f, qEndNeg);
273 :
274 2 : THEN( "result lands on positive 90 deg about Y — short arc was taken" ) {
275 1 : REQUIRE(result.getX() == Approx(0.0f));
276 1 : REQUIRE(result.getY() == Approx(s));
277 1 : REQUIRE(result.getZ() == Approx(0.0f));
278 1 : REQUIRE(result.getW() == Approx(c));
279 1 : }
280 1 : }
281 :
282 23 : GIVEN( "slerp between identical quaternions (linear fallback branch)" ) {
283 : // When start == end, dotQ == 1 and (1 - dotQ) == 0 <= 0.05 tolerance,
284 : // so the linear path (scale1 = 1-t, scale2 = t) is used.
285 : // The result must equal the input quaternion for any t.
286 1 : const float halfPi = 1.57079632679f;
287 1 : Quaternion q(Vector(0.0f, 1.0f, 0.0f), halfPi); // 90 deg about Y
288 :
289 1 : Quaternion result;
290 1 : result.slerp(q, 0.5f, q);
291 :
292 2 : THEN( "result equals the shared start/end quaternion" ) {
293 1 : REQUIRE(result.getX() == Approx(q.getX()));
294 1 : REQUIRE(result.getY() == Approx(q.getY()));
295 1 : REQUIRE(result.getZ() == Approx(q.getZ()));
296 1 : REQUIRE(result.getW() == Approx(q.getW()));
297 1 : }
298 1 : }
299 :
300 23 : GIVEN( "slerp endpoints" ) {
301 1 : Quaternion qStart;
302 1 : qStart.loadMultIdentity();
303 1 : Quaternion qEnd;
304 1 : qEnd.buildFromEuler(0.0f, 1.0f, 0.0f);
305 1 : qEnd.normalize();
306 :
307 1 : Quaternion atZero;
308 1 : atZero.slerp(qStart, 0.0f, qEnd);
309 1 : Quaternion atOne;
310 1 : atOne.slerp(qStart, 1.0f, qEnd);
311 :
312 2 : THEN( "t=0 matches start and t=1 matches end" ) {
313 1 : REQUIRE(atZero.getX() == Approx(qStart.getX()));
314 1 : REQUIRE(atZero.getY() == Approx(qStart.getY()));
315 1 : REQUIRE(atZero.getZ() == Approx(qStart.getZ()));
316 1 : REQUIRE(atZero.getW() == Approx(qStart.getW()));
317 1 : REQUIRE(atOne.getX() == Approx(qEnd.getX()));
318 1 : REQUIRE(atOne.getY() == Approx(qEnd.getY()));
319 1 : REQUIRE(atOne.getZ() == Approx(qEnd.getZ()));
320 1 : REQUIRE(atOne.getW() == Approx(qEnd.getW()));
321 1 : }
322 1 : }
323 22 : }
|