1/****************************************************************************
2**
3** Copyright (C) 2015 Klaralvdalens Datakonsult AB (KDAB).
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the Qt3D module of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:GPL-EXCEPT$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU
19** General Public License version 3 as published by the Free Software
20** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
21** included in the packaging of this file. Please review the following
22** information to ensure the GNU General Public License requirements will
23** be met: https://www.gnu.org/licenses/gpl-3.0.html.
24**
25** $QT_END_LICENSE$
26**
27****************************************************************************/
28
29#include <QtTest/QTest>
30#include <Qt3DAnimation/private/bezierevaluator_p.h>
31#include <Qt3DAnimation/private/keyframe_p.h>
32#include <QtCore/qvector.h>
33
34#include <cmath>
35
36Q_DECLARE_METATYPE(Qt3DAnimation::Animation::Keyframe)
37
38using namespace Qt3DAnimation;
39using namespace Qt3DAnimation::Animation;
40
41class tst_BezierEvaluator : public QObject
42{
43 Q_OBJECT
44
45private Q_SLOTS:
46 void checkFindCubicRoots_data()
47 {
48 // Test data verified on Wolfram Alpha with snippets such as:
49 // Plot[x^3-5x^2+x+3,{x,-3,6}]
50 // Solve[x^3-5x^2+x+3,x]
51 // If you need more, try these at https://www.wolframalpha.com/
52
53 QTest::addColumn<float>(name: "a");
54 QTest::addColumn<float>(name: "b");
55 QTest::addColumn<float>(name: "c");
56 QTest::addColumn<float>(name: "d");
57 QTest::addColumn<int>(name: "rootCount");
58 QTest::addColumn<QVector<float>>(name: "roots");
59
60 float a = 1.0f;
61 float b = 0.0f;
62 float c = 0.0f;
63 float d = 0.0f;
64 int rootCount = 1;
65 QVector<float> roots = { 0.0f };
66 QTest::newRow(dataTag: "a=1, b=0, c=0, d=0") << a << b << c << d << rootCount << roots;
67 roots.clear();
68
69 a = 1.0f;
70 b = -1.0f;
71 c = 1.0f;
72 d = -1.0f;
73 rootCount = 1;
74 roots.resize(asize: 1);
75 roots[0] = 1.0f;
76 QTest::newRow(dataTag: "a=1, b=-1, c=1, d=-1") << a << b << c << d << rootCount << roots;
77 roots.clear();
78
79 a = 1.0f;
80 b = -2.0f;
81 c = 1.0f;
82 d = -1.0f;
83 rootCount = 1;
84 roots.resize(asize: 1);
85 roots[0] = 1.7548776f;
86 QTest::newRow(dataTag: "a=1, b=-2, c=1, d=-1") << a << b << c << d << rootCount << roots;
87 roots.clear();
88
89 a = 1.0f;
90 b = -5.0f;
91 c = 1.0f;
92 d = 3.0f;
93 rootCount = 3;
94 roots.resize(asize: 3);
95 roots[0] = 2.0f + std::sqrt(x: 7.0f);
96 roots[1] = 1.0f;
97 roots[2] = 2.0f - std::sqrt(x: 7.0f);
98 QTest::newRow(dataTag: "a=1, b=-5, c=1, d=3") << a << b << c << d << rootCount << roots;
99 roots.clear();
100
101 // quadratic equation
102 a = 0.0f;
103 b = 9.0f;
104 c = 11.0f;
105 d = 3.0f;
106 roots.resize(asize: 2);
107 roots[0] = -11.0f/18.0f + std::sqrt(x: 13.0f) / 18.0f;
108 roots[1] = -11.0f/18.0f - std::sqrt(x: 13.0f) / 18.0f;
109 QTest::newRow(dataTag: "a=0, b=9, c=11, d=3") << a << b << c << d << roots.size() << roots;
110 roots.clear();
111
112 // quadratic equation with discriminant = 0
113 a = 0.0f;
114 b = 1.0f;
115 c = 2.0f;
116 d = 1.0f;
117 roots.resize(asize: 1);
118 roots[0] = -1.f;
119 QTest::newRow(dataTag: "a=0, b=1, c=2, d=1") << a << b << c << d << roots.size() << roots;
120 roots.clear();
121
122 // quadratic equation with discriminant < 0
123 a = 0.0f;
124 b = 1.0f;
125 c = 4.0f;
126 d = 8.0f;
127 roots.resize(asize: 0);
128 QTest::newRow(dataTag: "a=0, b=1, c=4, d=8") << a << b << c << d << roots.size() << roots;
129 roots.clear();
130
131 // linear equation
132 a = 0.0f;
133 b = 0.0f;
134 c = 2.0f;
135 d = 1.0f;
136 roots.resize(asize: 1);
137 roots[0] = -0.5f;
138 QTest::newRow(dataTag: "a=0, b=0, c=2, d=1") << a << b << c << d << roots.size() << roots;
139 roots.clear();
140
141 // linear equation
142 a = 0.0f;
143 b = 0.0f;
144 c = 8.0f;
145 d = -5.0f;
146 roots.resize(asize: 1);
147 roots[0] = -d/c;
148 QTest::newRow(dataTag: "a=0, b=0, c=8, d=-5") << a << b << c << d << roots.size() << roots;
149 roots.clear();
150
151 // invalid equation
152 a = 0.0f;
153 b = 0.0f;
154 c = 0.0f;
155 d = -5.0f;
156 roots.resize(asize: 0);
157 QTest::newRow(dataTag: "a=0, b=0, c=0, d=-5") << a << b << c << d << roots.size() << roots;
158 roots.clear();
159
160 // Invalid equation
161 a = 0.0f;
162 b = 0.0f;
163 c = 0.0f;
164 d = 42.0f;
165 roots.resize(asize: 0);
166 QTest::newRow(dataTag: "a=0, b=0, c=0, d=42") << a << b << c << d << roots.size() << roots;
167 roots.clear();
168
169 // almost linear equation
170 a = 1.90735e-06f;
171 b = -2.86102e-06f;
172 c = 5.0;
173 d = 0.0;
174 roots.resize(asize: 1);
175 roots[0] = -d/c;
176 QTest::newRow(dataTag: "a=~0, b=~0, c=5, d=0") << a << b << c << d << roots.size() << roots;
177 roots.clear();
178
179 // case that produces a result just below zero, that should be evaluated as zero
180 a = -0.75f;
181 b = 0.75f;
182 c = 2.5;
183 d = 0.0;
184 roots.resize(asize: 3);
185 roots[0] = 2.39297f;
186 roots[1] = 0.f;
187 roots[2] = -1.39297f;
188 QTest::newRow(dataTag: "a=-0.75, b=0.75, c=2.5, d=0") << a << b << c << d << roots.size() << roots;
189 roots.clear();
190
191 // Case that produces a discriminant that is close enough to zero that it should be
192 // evaluated as zero.
193 // Expected roots = 0.0, ~1.5
194 a = -3.998f;
195 b = 5.997f;
196 c = 0.0f;
197 d = 0.0f;
198 roots.resize(asize: 2);
199 roots[0] = 1.5f;
200 roots[1] = 0.0f;
201 QTest::newRow(dataTag: "a=-3.998, b=5.997, c=0, d=0") << a << b << c << d << roots.size() << roots;
202 roots.clear();
203 }
204
205 void checkFindCubicRoots()
206 {
207 QFETCH(float, a);
208 QFETCH(float, b);
209 QFETCH(float, c);
210 QFETCH(float, d);
211 QFETCH(int, rootCount);
212 QFETCH(QVector<float>, roots);
213
214 float coeffs[4];
215 coeffs[0] = d;
216 coeffs[1] = c;
217 coeffs[2] = b;
218 coeffs[3] = a;
219
220 float results[3];
221 const int foundRootCount = BezierEvaluator::findCubicRoots(coefficients: coeffs, roots: results);
222
223 QCOMPARE(foundRootCount, rootCount);
224 for (int i = 0; i < rootCount; ++i)
225 QCOMPARE(results[i], roots[i]);
226 }
227
228 void checkParameterForTime_data()
229 {
230 QTest::addColumn<float>(name: "t0");
231 QTest::addColumn<Keyframe>(name: "kf0");
232 QTest::addColumn<float>(name: "t1");
233 QTest::addColumn<Keyframe>(name: "kf1");
234 QTest::addColumn<QVector<float>>(name: "times");
235 QTest::addColumn<QVector<float>>(name: "bezierParamters");
236
237 {
238 float t0 = 0.0f;
239 Keyframe kf0{.value: 0.0f, .leftControlPoint: {-5.0f, 0.0f}, .rightControlPoint: {5.0f, 0.0f}, .interpolation: QKeyFrame::BezierInterpolation};
240 float t1 = 50.0f;
241 Keyframe kf1{.value: 5.0f, .leftControlPoint: {45.0f, 5.0f}, .rightControlPoint: {55.0f, 5.0f}, .interpolation: QKeyFrame::BezierInterpolation};
242 const int count = 21;
243 QVector<float> times = (QVector<float>()
244 << 0.0f
245 << 1.00375f
246 << 2.48f
247 << 4.37625f
248 << 6.64f
249 << 9.21875f
250 << 12.06f
251 << 15.11125f
252 << 18.32f
253 << 21.63375f
254 << 25.0f
255 << 28.36625f
256 << 31.68f
257 << 34.88875f
258 << 37.94f
259 << 40.78125f
260 << 43.36f
261 << 45.62375f
262 << 47.52f
263 << 48.99625f
264 << 50.0f);
265
266 QVector<float> bezierParameters;
267 float deltaU = 1.0f / float(count - 1);
268 for (int i = 0; i < count; ++i)
269 bezierParameters.push_back(t: float(i) * deltaU);
270
271 QTest::newRow(dataTag: "t=0 to t=50, default easing") << t0 << kf0
272 << t1 << kf1
273 << times << bezierParameters;
274 }
275 {
276 // This test creates a case where the coefficients for finding
277 // the cubic roots will be a = 0, b = 0, c ~= 6.28557 d ~= -6.28557
278 // Because c ~= d, the answer should be one root = 1, but
279 // because of numerical imprecision, it will be slightly larger.
280 // We have a fuzzy check in parameterForTime that takes care of this.
281 float t0 = 3.71443009f;
282 Keyframe kf0{.value: 150.0f, .leftControlPoint: {0.0f, 0.0f}, .rightControlPoint: {5.80961999f, 150.0f}, .interpolation: QKeyFrame::BezierInterpolation};
283 float t1 = 10.0f;
284 Keyframe kf1{.value: -150.0f, .leftControlPoint: {7.904809959f, 150.0f}, .rightControlPoint: {0.f, 0.f}, .interpolation: QKeyFrame::BezierInterpolation};
285 QVector<float> times = {10.f};
286 QVector<float> results = {1.0f};
287 QTest::newRow(dataTag: "t=0 to t=10, regression") << t0 << kf0
288 << t1 << kf1
289 << times << results;
290 }
291 }
292
293 void checkParameterForTime()
294 {
295 // GIVEN
296 QFETCH(float, t0);
297 QFETCH(Keyframe, kf0);
298 QFETCH(float, t1);
299 QFETCH(Keyframe, kf1);
300 QFETCH(QVector<float>, times);
301 QFETCH(QVector<float>, bezierParamters);
302
303 // WHEN
304 BezierEvaluator bezier(t0, kf0, t1, kf1);
305
306 // THEN
307 for (int i = 0; i < times.size(); ++i) {
308 const float time = times[i];
309 const float u = bezier.parameterForTime(time);
310 QCOMPARE(u, bezierParamters[i]);
311 }
312 }
313
314 void checkValueForTime_data()
315 {
316 QTest::addColumn<float>(name: "t0");
317 QTest::addColumn<Keyframe>(name: "kf0");
318 QTest::addColumn<float>(name: "t1");
319 QTest::addColumn<Keyframe>(name: "kf1");
320 QTest::addColumn<QVector<float>>(name: "times");
321 QTest::addColumn<QVector<float>>(name: "values");
322
323 float t0 = 0.0f;
324 Keyframe kf0{.value: 0.0f, .leftControlPoint: {-5.0f, 0.0f}, .rightControlPoint: {5.0f, 0.0f}, .interpolation: QKeyFrame::BezierInterpolation};
325 float t1 = 50.0f;
326 Keyframe kf1{.value: 5.0f, .leftControlPoint: {45.0f, 5.0f}, .rightControlPoint: {55.0f, 5.0f}, .interpolation: QKeyFrame::BezierInterpolation};
327 QVector<float> times = (QVector<float>()
328 << 0.0f
329 << 1.00375f
330 << 2.48f
331 << 4.37625f
332 << 6.64f
333 << 9.21875f
334 << 12.06f
335 << 15.11125f
336 << 18.32f
337 << 21.63375f
338 << 25.0f
339 << 28.36625f
340 << 31.68f
341 << 34.88875f
342 << 37.94f
343 << 40.78125f
344 << 43.36f
345 << 45.62375f
346 << 47.52f
347 << 48.99625f
348 << 50.0f);
349
350 QVector<float> values = (QVector<float>()
351 << 0.0f
352 << 0.03625f
353 << 0.14f
354 << 0.30375f
355 << 0.52f
356 << 0.78125f
357 << 1.08f
358 << 1.40875f
359 << 1.76f
360 << 2.12625f
361 << 2.5f
362 << 2.87375f
363 << 3.24f
364 << 3.59125f
365 << 3.92f
366 << 4.21875f
367 << 4.48f
368 << 4.69625f
369 << 4.86f
370 << 4.96375f
371 << 5.0f);
372
373 QTest::newRow(dataTag: "t=0, value=0 to t=50, value=5, default easing") << t0 << kf0
374 << t1 << kf1
375 << times << values;
376 }
377
378 void checkValueForTime()
379 {
380 // GIVEN
381 QFETCH(float, t0);
382 QFETCH(Keyframe, kf0);
383 QFETCH(float, t1);
384 QFETCH(Keyframe, kf1);
385 QFETCH(QVector<float>, times);
386 QFETCH(QVector<float>, values);
387
388 // WHEN
389 BezierEvaluator bezier(t0, kf0, t1, kf1);
390
391 // THEN
392 for (int i = 0; i < times.size(); ++i) {
393 const float time = times[i];
394 const float value = bezier.valueForTime(time);
395 QCOMPARE(value, values[i]);
396 }
397 }
398};
399
400QTEST_APPLESS_MAIN(tst_BezierEvaluator)
401
402#include "tst_bezierevaluator.moc"
403

source code of qt3d/tests/auto/animation/bezierevaluator/tst_bezierevaluator.cpp