1//************************************ bs::framework - Copyright 2018 Marko Pintera **************************************//
2//*********** Licensed under the MIT license. See LICENSE.md for full terms. This notice is not to be removed. ***********//
3#include "Animation/BsAnimationUtility.h"
4#include "Math/BsVector3.h"
5#include "Math/BsQuaternion.h"
6
7namespace bs
8{
9 void setStepTangent(const TKeyframe<Vector3>& lhsIn, const TKeyframe<Vector3>& rhsIn,
10 TKeyframe<Quaternion>& lhsOut, TKeyframe<Quaternion>& rhsOut)
11 {
12 for (UINT32 i = 0; i < 3; i++)
13 {
14 if (lhsIn.outTangent[i] != std::numeric_limits<float>::infinity() &&
15 rhsIn.inTangent[i] != std::numeric_limits<float>::infinity())
16 continue;
17
18 lhsOut.outTangent[i] = std::numeric_limits<float>::infinity();
19 rhsOut.inTangent[i] = std::numeric_limits<float>::infinity();
20 }
21 }
22
23 void setStepTangent(const TKeyframe<Quaternion>& lhsIn, const TKeyframe<Quaternion>& rhsIn,
24 TKeyframe<Vector3>& lhsOut, TKeyframe<Vector3>& rhsOut)
25 {
26 for (UINT32 i = 0; i < 4; i++)
27 {
28 if (lhsIn.outTangent[i] != std::numeric_limits<float>::infinity() &&
29 rhsIn.inTangent[i] != std::numeric_limits<float>::infinity())
30 continue;
31
32 if (i < 3)
33 {
34 lhsOut.outTangent[i] = std::numeric_limits<float>::infinity();
35 rhsOut.inTangent[i] = std::numeric_limits<float>::infinity();
36 }
37 }
38 }
39
40 void AnimationUtility::wrapTime(float& time, float start, float end, bool loop)
41 {
42 float length = end - start;
43
44 if(Math::approxEquals(length, 0.0f))
45 {
46 time = 0.0f;
47 return;
48 }
49
50 // Clamp to start or loop
51 if (time < start)
52 {
53 if (loop)
54 time = time + (std::floor(end - time) / length) * length;
55 else // Clamping
56 time = start;
57 }
58
59 // Clamp to end or loop
60 if (time > end)
61 {
62 if (loop)
63 time = time - std::floor((time - start) / length) * length;
64 else // Clamping
65 time = end;
66 }
67 }
68
69 SPtr<TAnimationCurve<Quaternion>> AnimationUtility::eulerToQuaternionCurve(
70 const SPtr<TAnimationCurve<Vector3>>& eulerCurve, EulerAngleOrder order)
71 {
72 // TODO: We calculate tangents by sampling which can introduce error in the tangents. The error can be exacerbated
73 // by the fact we constantly switch between the two representations, possibly losing precision every time. Instead
74 // there must be an analytical way to calculate tangents when converting a curve, or a better way of dealing with
75 // tangents.
76 // Consider:
77 // - Sampling multiple points to calculate tangents to improve precision
78 // - Store the original quaternion curve with the euler curve
79 // - This way conversion from euler to quaternion can be done while individual keyframes are being modified
80 // ensuring the conversion results are immediately visible, and that no accumulation error happens are curves
81 // are converted between two formats back and forth.
82 // - Don't store rotation tangents directly, instead store tangent parameters (TCB) which can be shared between
83 // both curves, and used for tangent calculation.
84 //
85 // If we decide to keep tangents in the current form, then we should also enforce that all euler curve tangents are
86 // the same.
87 const float FIT_TIME = 0.001f;
88
89 auto eulerToQuaternion = [&](INT32 keyIdx, Vector3& angles, const Quaternion& lastQuat)
90 {
91 Quaternion quat(
92 Degree(angles.x),
93 Degree(angles.y),
94 Degree(angles.z), order);
95
96 // Flip quaternion in case rotation is over 180 degrees (use shortest path)
97 if (keyIdx > 0)
98 {
99 float dot = quat.dot(lastQuat);
100 if (dot < 0.0f)
101 quat = -quat;
102 }
103
104 return quat;
105 };
106
107 INT32 numKeys = (INT32)eulerCurve->getNumKeyFrames();
108 Vector<TKeyframe<Quaternion>> quatKeyframes(numKeys);
109
110 // Calculate key values
111 Quaternion lastQuat(BsZero);
112 for (INT32 i = 0; i < numKeys; i++)
113 {
114 float time = eulerCurve->getKeyFrame(i).time;
115 Vector3 angles = eulerCurve->getKeyFrame(i).value;
116 Quaternion quat = eulerToQuaternion(i, angles, lastQuat);
117
118 quatKeyframes[i].time = time;
119 quatKeyframes[i].value = quat;
120 quatKeyframes[i].inTangent = Quaternion::ZERO;
121 quatKeyframes[i].outTangent = Quaternion::ZERO;
122
123 lastQuat = quat;
124 }
125
126 // Calculate extra values between keys so we can approximate tangents. If we're sampling very close to the key
127 // the values should pretty much exactly match the tangent (assuming the curves are cubic hermite)
128 for (INT32 i = 0; i < numKeys - 1; i++)
129 {
130 TKeyframe<Quaternion>& currentKey = quatKeyframes[i];
131 TKeyframe<Quaternion>& nextKey = quatKeyframes[i + 1];
132
133 const TKeyframe<Vector3>& currentEulerKey = eulerCurve->getKeyFrame(i);
134 const TKeyframe<Vector3>& nextEulerKey = eulerCurve->getKeyFrame(i + 1);
135
136 float dt = nextKey.time - currentKey.time;
137 float startFitTime = currentKey.time + dt * FIT_TIME;
138 float endFitTime = currentKey.time + dt * (1.0f - FIT_TIME);
139
140 Vector3 anglesStart = eulerCurve->evaluate(startFitTime, false);
141 Vector3 anglesEnd = eulerCurve->evaluate(endFitTime, false);
142 Quaternion startFitValue = eulerToQuaternion(i, anglesStart, currentKey.value);
143 Quaternion endFitValue = eulerToQuaternion(i, anglesEnd, startFitValue);
144
145 float invFitTime = 1.0f / (dt * FIT_TIME);
146 currentKey.outTangent = (startFitValue - currentKey.value) * invFitTime;
147 nextKey.inTangent = (nextKey.value - endFitValue) * invFitTime;
148
149 setStepTangent(currentEulerKey, nextEulerKey, currentKey, nextKey);
150 }
151
152 return bs_shared_ptr_new<TAnimationCurve<Quaternion>>(quatKeyframes);
153 }
154
155 SPtr<TAnimationCurve<Vector3>> AnimationUtility::quaternionToEulerCurve(const SPtr<TAnimationCurve<Quaternion>>& quatCurve)
156 {
157 // TODO: We calculate tangents by sampling. There must be an analytical way to calculate tangents when converting
158 // a curve.
159 const float FIT_TIME = 0.001f;
160
161 auto quaternionToEuler = [&](const Quaternion& quat)
162 {
163 Radian x, y, z;
164 quat.toEulerAngles(x, y, z);
165
166 Vector3 euler(
167 x.valueDegrees(),
168 y.valueDegrees(),
169 z.valueDegrees()
170 );
171
172 return euler;
173 };
174
175 INT32 numKeys = (INT32)quatCurve->getNumKeyFrames();
176 Vector<TKeyframe<Vector3>> eulerKeyframes(numKeys);
177
178 // Calculate key values
179 for (INT32 i = 0; i < numKeys; i++)
180 {
181 float time = quatCurve->getKeyFrame(i).time;
182 Quaternion quat = quatCurve->getKeyFrame(i).value;
183 Vector3 euler = quaternionToEuler(quat);
184
185 eulerKeyframes[i].time = time;
186 eulerKeyframes[i].value = euler;
187 eulerKeyframes[i].inTangent = Vector3::ZERO;
188 eulerKeyframes[i].outTangent = Vector3::ZERO;
189 }
190
191 // Calculate extra values between keys so we can approximate tangents. If we're sampling very close to the key
192 // the values should pretty much exactly match the tangent (assuming the curves are cubic hermite)
193 for (INT32 i = 0; i < numKeys - 1; i++)
194 {
195 TKeyframe<Vector3>& currentKey = eulerKeyframes[i];
196 TKeyframe<Vector3>& nextKey = eulerKeyframes[i + 1];
197
198 const TKeyframe<Quaternion>& currentQuatKey = quatCurve->getKeyFrame(i);
199 const TKeyframe<Quaternion>& nextQuatKey = quatCurve->getKeyFrame(i + 1);
200
201 float dt = nextKey.time - currentKey.time;
202 float startFitTime = currentKey.time + dt * FIT_TIME;
203 float endFitTime = currentKey.time + dt * (1.0f - FIT_TIME);
204
205 Quaternion startQuat = Quaternion::normalize(quatCurve->evaluate(startFitTime, false));
206 Quaternion endQuat = Quaternion::normalize(quatCurve->evaluate(endFitTime, false));
207 Vector3 startFitValue = quaternionToEuler(startQuat);
208 Vector3 endFitValue = quaternionToEuler(endQuat);
209
210 // If fit values rotate for more than 180 degrees, wrap them so they use the shortest path
211 for(int j = 0; j < 3; j++)
212 {
213 startFitValue[j] = fmod(startFitValue[j] - currentKey.value[j] + 180.0f, 360.0f) + currentKey.value[j] - 180.0f;
214 endFitValue[j] = nextKey.value[j] + fmod(nextKey.value[j] - endFitValue[j] + 180.0f, 360.0f) - 180.0f;
215 }
216
217 float invFitTime = 1.0f / (dt * FIT_TIME);
218 currentKey.outTangent = (startFitValue - currentKey.value) * invFitTime;
219 nextKey.inTangent = (nextKey.value - endFitValue) * invFitTime;
220
221 setStepTangent(currentQuatKey, nextQuatKey, currentKey, nextKey);
222 }
223
224 return bs_shared_ptr_new<TAnimationCurve<Vector3>>(eulerKeyframes);
225 }
226
227 template <class T>
228 void splitCurve(
229 const TAnimationCurve<T>& compoundCurve,
230 Vector<TKeyframe<float>> (&keyFrames)[TCurveProperties<T>::NumComponents])
231 {
232 constexpr UINT32 NUM_COMPONENTS = TCurveProperties<T>::NumComponents;
233
234 const UINT32 numKeyFrames = compoundCurve.getNumKeyFrames();
235 for (UINT32 i = 0; i < numKeyFrames; i++)
236 {
237 const TKeyframe<T>& key = compoundCurve.getKeyFrame(i);
238
239 TKeyframe<float> newKey;
240 newKey.time = key.time;
241
242 for (UINT32 j = 0; j < NUM_COMPONENTS; j++)
243 {
244 bool addNew = true;
245 if (i > 0)
246 {
247 const TKeyframe<float>& prevKey = keyFrames[j].back();
248
249 bool isEqual = Math::approxEquals(prevKey.value, TCurveProperties<T>::getComponent(key.value, j)) &&
250 Math::approxEquals(prevKey.outTangent, TCurveProperties<T>::getComponent(key.inTangent, j));
251
252 addNew = !isEqual;
253 }
254
255 if (addNew)
256 {
257 newKey.value = TCurveProperties<T>::getComponent(key.value, j);
258 newKey.inTangent = TCurveProperties<T>::getComponent(key.inTangent, j);
259 newKey.outTangent = TCurveProperties<T>::getComponent(key.outTangent, j);
260
261 keyFrames[j].push_back(newKey);
262 }
263 }
264 }
265 }
266
267 template <class T>
268 void combineCurve(
269 const TAnimationCurve<float>* (& curveComponents)[TCurveProperties<T>::NumComponents],
270 Vector<TKeyframe<T>>& output)
271 {
272 constexpr UINT32 NUM_COMPONENTS = TCurveProperties<T>::NumComponents;
273
274 // Find unique keyframe times
275 Map<float, TKeyframe<T>> keyFrames;
276 for(UINT32 i = 0; i < NUM_COMPONENTS; i++)
277 {
278 UINT32 numKeyFrames = curveComponents[i]->getNumKeyFrames();
279 for (UINT32 j = 0; j < numKeyFrames; j++)
280 {
281 const TKeyframe<float>& keyFrame = curveComponents[i]->getKeyFrame(j);
282
283 auto iterFind = keyFrames.find(keyFrame.time);
284 if (iterFind == keyFrames.end())
285 {
286 TKeyframe<T> newKeyFrame;
287 newKeyFrame.time = keyFrame.time;
288
289 keyFrames.insert(std::make_pair(keyFrame.time, newKeyFrame));
290 }
291 }
292 }
293
294 // Populate keyframe values
295 output.resize(keyFrames.size());
296 UINT32 idx = 0;
297 for(auto& entry : keyFrames)
298 {
299 TKeyframe<T>& keyFrame = entry.second;
300
301 for(UINT32 j = 0; j < NUM_COMPONENTS; j++)
302 {
303 TKeyframe<float> currentKey = curveComponents[j]->evaluateKey(keyFrame.time, false);
304 TCurveProperties<T>::setComponent(keyFrame.value, j, currentKey.value);
305 TCurveProperties<T>::setComponent(keyFrame.inTangent, j, currentKey.inTangent);
306 TCurveProperties<T>::setComponent(keyFrame.outTangent, j, currentKey.outTangent);
307 }
308
309 output[idx] = keyFrame;
310 idx++;
311 }
312 }
313
314 Vector<SPtr<TAnimationCurve<float>>> AnimationUtility::splitCurve3D(const SPtr<TAnimationCurve<Vector3>>& compoundCurve)
315 {
316 Vector<TKeyframe<float>> keyFrames[3];
317
318 if(compoundCurve)
319 bs::splitCurve(*compoundCurve, keyFrames);
320
321 Vector<SPtr<TAnimationCurve<float>>> output(3);
322 for (UINT32 i = 0; i < 3; i++)
323 output[i] = bs_shared_ptr_new<TAnimationCurve<float>>(keyFrames[i]);
324
325 return output;
326 }
327
328 SPtr<TAnimationCurve<Vector3>> AnimationUtility::combineCurve3D(const Vector<SPtr<TAnimationCurve<float>>>& curveComponents)
329 {
330 Vector<TKeyframe<Vector3>> keyFrames;
331 if(curveComponents.size() >= 3)
332 {
333 const TAnimationCurve<float>* curves[] =
334 { curveComponents[0].get(), curveComponents[1].get(), curveComponents[2].get() };
335
336 bs::combineCurve(curves, keyFrames);
337 }
338
339 return bs_shared_ptr_new<TAnimationCurve<Vector3>>(keyFrames);
340 }
341
342 Vector<SPtr<TAnimationCurve<float>>> AnimationUtility::splitCurve2D(const SPtr<TAnimationCurve<Vector2>>& compoundCurve)
343 {
344 Vector<TKeyframe<float>> keyFrames[2];
345
346 if(compoundCurve)
347 bs::splitCurve(*compoundCurve, keyFrames);
348
349 Vector<SPtr<TAnimationCurve<float>>> output(2);
350 for (UINT32 i = 0; i < 2; i++)
351 output[i] = bs_shared_ptr_new<TAnimationCurve<float>>(keyFrames[i]);
352
353 return output;
354 }
355
356 SPtr<TAnimationCurve<Vector2>> AnimationUtility::combineCurve2D(const Vector<SPtr<TAnimationCurve<float>>>& curveComponents)
357 {
358 Vector<TKeyframe<Vector2>> keyFrames;
359 if(curveComponents.size() >= 2)
360 {
361 const TAnimationCurve<float>* curves[] =
362 { curveComponents[0].get(), curveComponents[1].get() };
363
364 bs::combineCurve(curves, keyFrames);
365 }
366
367 return bs_shared_ptr_new<TAnimationCurve<Vector2>>(keyFrames);
368 }
369
370 template <class T>
371 void AnimationUtility::splitCurve(const TAnimationCurve<T>& compoundCurve,
372 TAnimationCurve<float> (& output)[TCurveProperties<T>::NumComponents])
373 {
374 constexpr UINT32 NUM_COMPONENTS = TCurveProperties<T>::NumComponents;
375
376 Vector<TKeyframe<float>> keyFrames[NUM_COMPONENTS];
377 bs::splitCurve(compoundCurve, keyFrames);
378
379 for (UINT32 i = 0; i < NUM_COMPONENTS; i++)
380 output[i] = TAnimationCurve<float>(keyFrames[i]);
381 }
382
383 template <class T>
384 void AnimationUtility::combineCurve(
385 const TAnimationCurve<float> (& curveComponents)[TCurveProperties<T>::NumComponents],
386 TAnimationCurve<T>& output)
387 {
388 constexpr UINT32 NUM_COMPONENTS = TCurveProperties<T>::NumComponents;
389
390 const TAnimationCurve<float>* curves[NUM_COMPONENTS];
391 for(UINT32 i = 0; i < NUM_COMPONENTS; i++)
392 curves[i] = &curveComponents[i];
393
394 Vector<TKeyframe<T>> keyFrames;
395 bs::combineCurve(curves, keyFrames);
396
397 output = TAnimationCurve<T>(keyFrames);
398 }
399
400 void AnimationUtility::calculateRange(const Vector<TAnimationCurve<float>>& curves, float& xMin, float& xMax,
401 float& yMin, float& yMax)
402 {
403 xMin = std::numeric_limits<float>::infinity();
404 xMax = -std::numeric_limits<float>::infinity();
405 yMin = std::numeric_limits<float>::infinity();
406 yMax = -std::numeric_limits<float>::infinity();
407
408 for(auto& entry : curves)
409 {
410 const auto timeRange = entry.getTimeRange();
411 const auto valueRange = entry.calculateRange();
412
413 xMin = std::min(xMin, timeRange.first);
414 xMax = std::max(xMax, timeRange.second);
415 yMin = std::min(yMin, valueRange.first);
416 yMax = std::max(yMax, valueRange.second);
417 }
418
419 if (xMin == std::numeric_limits<float>::infinity())
420 xMin = 0.0f;
421
422 if (xMax == -std::numeric_limits<float>::infinity())
423 xMax = 0.0f;
424
425 if (yMin == std::numeric_limits<float>::infinity())
426 yMin = 0.0f;
427
428 if (yMax == -std::numeric_limits<float>::infinity())
429 yMax = 0.0f;
430 }
431
432 void AnimationUtility::calculateRange(const Vector<SPtr<TAnimationCurve<float>>>& curves, float& xMin, float& xMax,
433 float& yMin, float& yMax)
434 {
435 xMin = std::numeric_limits<float>::infinity();
436 xMax = -std::numeric_limits<float>::infinity();
437 yMin = std::numeric_limits<float>::infinity();
438 yMax = -std::numeric_limits<float>::infinity();
439
440 for(auto& entry : curves)
441 {
442 const auto timeRange = entry->getTimeRange();
443 const auto valueRange = entry->calculateRange();
444
445 xMin = std::min(xMin, timeRange.first);
446 xMax = std::max(xMax, timeRange.second);
447 yMin = std::min(yMin, valueRange.first);
448 yMax = std::max(yMax, valueRange.second);
449 }
450
451 if (xMin == std::numeric_limits<float>::infinity())
452 xMin = 0.0f;
453
454 if (xMax == -std::numeric_limits<float>::infinity())
455 xMax = 0.0f;
456
457 if (yMin == std::numeric_limits<float>::infinity())
458 yMin = 0.0f;
459
460 if (yMax == -std::numeric_limits<float>::infinity())
461 yMax = 0.0f;
462 }
463
464 template<class T>
465 TAnimationCurve<T> AnimationUtility::scaleCurve(const TAnimationCurve<T>& curve, float factor)
466 {
467 INT32 numKeys = (INT32)curve.getNumKeyFrames();
468
469 Vector<TKeyframe<T>> newKeyframes(numKeys);
470 for (INT32 i = 0; i < numKeys; i++)
471 {
472 const TKeyframe<T>& key = curve.getKeyFrame(i);
473 newKeyframes[i].time = key.time;
474 newKeyframes[i].value = key.value * factor;
475 newKeyframes[i].inTangent = key.inTangent * factor;
476 newKeyframes[i].outTangent = key.outTangent * factor;
477 }
478
479 return TAnimationCurve<T>(newKeyframes);
480 }
481
482 template<class T>
483 TAnimationCurve<T> AnimationUtility::offsetCurve(const TAnimationCurve<T>& curve, float offset)
484 {
485 INT32 numKeys = (INT32)curve.getNumKeyFrames();
486
487 Vector<TKeyframe<T>> newKeyframes(numKeys);
488 for (INT32 i = 0; i < numKeys; i++)
489 {
490 const TKeyframe<T>& key = curve.getKeyFrame(i);
491 newKeyframes[i].time = key.time + offset;
492 newKeyframes[i].value = key.value;
493 newKeyframes[i].inTangent = key.inTangent;
494 newKeyframes[i].outTangent = key.outTangent;
495 }
496
497 return TAnimationCurve<T>(newKeyframes);
498 }
499
500 template <class T>
501 void AnimationUtility::calculateTangents(Vector<TKeyframe<T>>& keyframes)
502 {
503 using Keyframe = TKeyframe<T>;
504 if (keyframes.empty())
505 return;
506
507 if (keyframes.size() == 1)
508 {
509 keyframes[0].inTangent = TCurveProperties<T>::getZero();
510 keyframes[0].outTangent = TCurveProperties<T>::getZero();
511
512 return;
513 }
514
515 auto calcTangent = [](const Keyframe& left, const Keyframe& right)
516 {
517 float diff = right.time - left.time;
518
519 if (!Math::approxEquals(diff, 0.0f))
520 return (right.value - left.value) / diff;
521
522 return std::numeric_limits<T>::infinity();
523 };
524
525 // First keyframe
526 {
527 Keyframe& keyThis = keyframes[0];
528 const Keyframe& keyNext = keyframes[1];
529
530 keyThis.inTangent = TCurveProperties<T>::getZero();
531 keyThis.outTangent = calcTangent(keyThis, keyNext);
532 }
533
534 // Inner keyframes
535 for (UINT32 i = 1; i < (UINT32)keyframes.size() - 1; i++)
536 {
537 const Keyframe& keyPrev = keyframes[i - 1];
538 Keyframe& keyThis = keyframes[i];
539 const Keyframe& keyNext = keyframes[i + 1];
540
541 keyThis.outTangent = calcTangent(keyPrev, keyNext);
542 keyThis.inTangent = keyThis.outTangent;
543 }
544
545 // Last keyframe
546 {
547 Keyframe& keyThis = keyframes[keyframes.size() - 1];
548 const Keyframe& keyPrev = keyframes[keyframes.size() - 2];
549
550 keyThis.outTangent = TCurveProperties<T>::getZero();
551 keyThis.inTangent = calcTangent(keyPrev, keyThis);
552 }
553 }
554
555 template BS_CORE_EXPORT TAnimationCurve<Vector3> AnimationUtility::scaleCurve(const TAnimationCurve<Vector3>& curve, float factor);
556 template BS_CORE_EXPORT TAnimationCurve<Vector2> AnimationUtility::scaleCurve(const TAnimationCurve<Vector2>& curve, float factor);
557 template BS_CORE_EXPORT TAnimationCurve<Quaternion> AnimationUtility::scaleCurve(const TAnimationCurve<Quaternion>& curve, float factor);
558 template BS_CORE_EXPORT TAnimationCurve<float> AnimationUtility::scaleCurve(const TAnimationCurve<float>& curve, float factor);
559
560 template BS_CORE_EXPORT TAnimationCurve<Vector3> AnimationUtility::offsetCurve(const TAnimationCurve<Vector3>& curve, float offset);
561 template BS_CORE_EXPORT TAnimationCurve<Vector2> AnimationUtility::offsetCurve(const TAnimationCurve<Vector2>& curve, float offset);
562 template BS_CORE_EXPORT TAnimationCurve<Quaternion> AnimationUtility::offsetCurve(const TAnimationCurve<Quaternion>& curve, float offset);
563 template BS_CORE_EXPORT TAnimationCurve<float> AnimationUtility::offsetCurve(const TAnimationCurve<float>& curve, float offset);
564
565 template BS_CORE_EXPORT void AnimationUtility::calculateTangents(Vector<TKeyframe<Vector3>>& keyframes);
566 template BS_CORE_EXPORT void AnimationUtility::calculateTangents(Vector<TKeyframe<Vector2>>& keyframes);
567 template BS_CORE_EXPORT void AnimationUtility::calculateTangents(Vector<TKeyframe<Quaternion>>& keyframes);
568 template BS_CORE_EXPORT void AnimationUtility::calculateTangents(Vector<TKeyframe<float>>& keyframes);
569
570 template BS_CORE_EXPORT void AnimationUtility::splitCurve(const TAnimationCurve<float>&, TAnimationCurve<float> (&)[1]);
571 template BS_CORE_EXPORT void AnimationUtility::splitCurve(const TAnimationCurve<Vector2>&, TAnimationCurve<float> (&)[2]);
572 template BS_CORE_EXPORT void AnimationUtility::splitCurve(const TAnimationCurve<Vector3>&, TAnimationCurve<float> (&)[3]);
573
574 template BS_CORE_EXPORT void AnimationUtility::combineCurve(const TAnimationCurve<float> (&)[1], TAnimationCurve<float>&);
575 template BS_CORE_EXPORT void AnimationUtility::combineCurve(const TAnimationCurve<float> (&)[2], TAnimationCurve<Vector2>&);
576 template BS_CORE_EXPORT void AnimationUtility::combineCurve(const TAnimationCurve<float> (&)[3], TAnimationCurve<Vector3>&);
577}
578