UDocumentation UE5.7 10.02.2026 (Source)
API documentation for Unreal Engine 5.7
TimeStretchCurve.h
Go to the documentation of this file.
1// Copyright Epic Games, Inc. All Rights Reserved.
2
3#pragma once
4
5#include "CoreMinimal.h"
7#include "UObject/Class.h"
9#include "TimeStretchCurve.generated.h"
10
11UENUM()
13{
14 T_Original = 0,
15 T_TargetMin = 1,
16 T_TargetMax = 2,
17 MAX = 3
18};
19
20USTRUCT()
47
48/*
49 = Time Stretch Curve =
50
51 = What is it?
52
53 The Time Stretch Curve is an optional float curve that montages can
54 use to define where a montage is allowed to speed up or slow down.
55 Let's say we have a montage of default play time T_Original.
56 We now want that montage to play for a different T_Target play time
57 Typically we would uniformly play rate the animation to reach that goal.
58
59 The Time Stretch Curve allows doing the same thing but non uniformly,
60 by defining which regions can be play rated more or less.
61
62 The Curve values are normalized.
63 So a Curve value of 0 means "don't play rate this".
64 And a Curve value of 1 means "play rate this the most".
65 Intermediate values will be weighted accordingly.
66
67 Imagine the following scenario, you have a character attacking with a staff.
68 The animation is authored with holds after striking.
69 Let's say the character levels up over the course of the game, and
70 their attacks are getting faster and faster (play time is shorter).
71
72 By using a Time Stretch Curve, most of the time compression could happen
73 during the holds. So the strikes look mostly unaffected.
74 This allows using a single animation, and scaling it for very different
75 desired play times.
76
77 = How does it work?
78
79 Given a Montage of length T_Original, and a float curve C.
80 Curve C is sampled over N segments of fixed time 'SamplingTimeStep'.
81 Each element, C_i is normalized. 0 <= C_i <= 1
82 and 0 <= i <= N.
83
84 We have Sum(SamplingTimeStep) = T_Original = N * SamplingTimeStep
85 SamplingTimeStep = T_Original / N
86
87 Given a different length T_Target,
88 C remaps positions from T_Target to T_Original according to the following function:
89 dTO = dT_i * U * (1 + S * C_i)
90 where:
91 Sum(dTO) = T_Original
92 Sum(dT_i) = T_Target
93 U = UniformPlayRate
94 S = Curve Scale Factor
95 C_i = sampled curve value, constant over the interval dTO
96
97 dTO is fixed, based on the chosen 'SamplingTimeStep',
98 but in practice we can combine consecutive segments that have the same C_i value.
99
100 We would like U to be 1 (or -1) as much as possible.
101 Meaning the Curve should do all the remapping whenever possible.
102 U(niformPlayRate) should only come into play when C can't remap T_Target to T_Original on its own.
103 This can happen when trying to speed up the animation,
104 but the Curve is not enough to reach that time compression.
105 In that event, uniform scaling kicks in.
106
107 We call PR_i (or OverallPlayRate for Segment i)
108 PR_i = U * (1 + S * C_i)
109 dTO = dT_i * PR_i
110
111 We also want PR_i > 0, that is it should never backtrack or pause during playback of animation.
112 A Montage can still play in reverse with U < 0.
113
114 = How is T_Target defined?
115
116 When we play a Montage with a PlayRate of PR, we assume this means:
117 T_Target = T_Original * PR
118 So this system does not change the interface for playing a montage.
119
120 If a curve is not defined, the mapping is defined as:
121 dTO_i = dT_i * U
122
123 If a a curve is defined:
124 dTO_i = dT_i * U * (1 + S * C_i)
125
126 We can see that no curve means S == 0.
127 Also if we're not scaling the montage, T_Target == T_Original, we also have S == 0.
128
129 So, this makes the curve completely optional. And it seamlessly integrates with the current montage interface.
130 If you want playback time to be half, that means playing the montage with a play rate of 2.
131 If there is a TimeStretchCurve, it will be used.
132 But regardless or using a curve or not, play back time is guaranteed.
133
134 = Finding U and S =
135
136 Ideally, we could figure out what U and S are given a T_Target play time.
137 Unfortunately, the math for this is very complex.
138
139 We update the montage position like this:
140 dTO_i = dT_i * U * (1 + C_i)
141
142 We sum this over the N time segments:
143 Sum(dTO_i) = Sum(dT_i * U * (1 + C_i))
144 Sum(dTO_i) = Sum(dT_i) * U + Sum(dT_i * U * S * C_i))
145 T_Original = T_Target * U + U * S * Sum(dT_i * C_i)
146
147 Where:
148 S = (T_Original - T_Target * U) / (U * Sum(dT_i * C_i))
149
150 If we were able to get dT_i constant, we could pull it out and get:
151 S = (T_Original - T_Target * U) / (U * dT * Sum(C_i))
152 Where Sum(C_i) can be pre-computed.
153
154 Unfortunately we don't have dT_i constant, and we can't,
155 since it is variable, and dependent on what S and U are.
156
157 So our approach instead is to precompute lower and upper bounds for this curve.
158 We cache the results for dT_i for S = 100.f and S = -1.f + 0.01f
159 This gives us data for T_Target_Min and T_Target_Max.
160 Within these bounds, we can approximate dT_i, and also Sum(dT_i * C_i) by linear interpolation.
161 Outside of these bounds, we rely on U to get us to our desired T_Target play back time.
162
163 'ConditionallyUpdateTimeStretchCurveCachedData' takes care of figuring out U and S
164 based on a given T_Target play back time.
165
166 = 'target' and 'original' space
167
168 At run time, we generate a set of markers in what we call 'target' and 'original' space.
169 'original' space means in the original play time the montage was authored in.
170 So that maps to actual frames of animation and actual position in the montage.
171
172 'target' space is the same set of markers, but mapped in play back time.
173 That is the actual time it takes to play back this montage.
174
175 Taking our play rate equation from above, it is:
176 dT_Original = dT_Target * U * (1 + S * C_i)
177
178 We see that montage position = playback time * play rate.
179
180 Once we have mapped our markers in both 'target' and 'original' space,
181 we can easily go from one to the other, since time moves linearly between markers.
182 Since between markers the play rate is defined as constant values:
183 PR_i = U * (1.f + S * C_i).
184 And we know that C_i is constant between two markers.
185
186 So if we know between which markers we are in one space, we can switch to the other space instantly,
187 as our relative position between these markers will be the same.
188
189 So by jumping between these spaces, we can quickly go from a montage position to its playback time.
190 We can increase the playback time by the current's frame deltatime,
191 and jump back to the corresponding frame of animation.
192 That's in a nutshell how this system works.
193*/
194
195USTRUCT()
197{
199
201
202public:
204 : SamplingRate(60.f)
205 , CurveValueMinPrecision(0.01f)
206 {
207 Sum_dT_i_by_C_i[(uint8)ETimeStretchCurveMapping::T_Original] = 0.0f;
208 Sum_dT_i_by_C_i[(uint8)ETimeStretchCurveMapping::T_TargetMin] = 0.0f;
209 Sum_dT_i_by_C_i[(uint8)ETimeStretchCurveMapping::T_TargetMax] = 0.0f;
210 }
211
212 ENGINE_API bool IsValid() const;
213 ENGINE_API void Reset();
214 ENGINE_API void BakeFromFloatCurve(const FFloatCurve& TimeStretchCurve, float InSequenceLength);
215
216private:
222 UPROPERTY(EditAnywhere, Category = TimeStretchCurve)
223 float SamplingRate;
224
230 UPROPERTY(EditAnywhere, Category = TimeStretchCurve)
231 float CurveValueMinPrecision;
232
234 UPROPERTY(VisibleAnywhere, Category = TimeStretchCurve)
236
238 UPROPERTY(VisibleAnywhere, Category = TimeStretchCurve)
239 float Sum_dT_i_by_C_i[(uint8)ETimeStretchCurveMapping::MAX];
240};
241
242USTRUCT()
244{
246
247public:
249 : bHasValidData(false)
250 {}
251
253 bool HasValidData() const { return bHasValidData; }
254
260
263
266
272
279
286
288 ENGINE_API float Clamp_P_Target(float In_P_Target) const;
289
291 const TArray<float>& GetMarkers_Original() const { return P_Marker_Original; }
292
294 const TArray<float>& GetMarkers_Target() const { return P_Marker_Target; }
295
297 float Get_T_Original() const { return T_Original; }
298
300 float Get_T_Target() const { return T_Target; }
301
302private:
304 bool bHasValidData;
305
306 float T_Original;
307 float T_Target;
308
309 TArray<float> P_Marker_Original;
310 TArray<float> P_Marker_Target;
311};
FPlatformTypes::int32 int32
A 32-bit signed integer.
Definition Platform.h:1125
UE_FORCEINLINE_HINT TSharedRef< CastToType, Mode > StaticCastSharedRef(TSharedRef< CastFromType, Mode > const &InSharedRef)
Definition SharedPointer.h:127
#define UPROPERTY(...)
UObject definition macros.
Definition ObjectMacros.h:744
#define UENUM(...)
Definition ObjectMacros.h:749
#define USTRUCT(...)
Definition ObjectMacros.h:746
#define GENERATED_USTRUCT_BODY(...)
Definition ObjectMacros.h:767
USkinnedMeshComponent float
Definition SkinnedMeshComponent.h:60
ETimeStretchCurveMapping
Definition TimeStretchCurve.h:13
uint8_t uint8
Definition binka_ue_file_header.h:8
Definition Array.h:670
@ false
Definition radaudio_common.h:23
Definition AnimCurveTypes.h:214
Definition TimeStretchCurve.h:244
ENGINE_API bool IsValidMarkerForPosition(int32 InMarkerIndex, float InPosition, const TArray< float > &InMarkerPositions) const
Definition TimeStretchCurve.cpp:401
ENGINE_API void InitializeFromPlayRate(float InPlayRate, const FTimeStretchCurve &TimeStretchCurve)
Definition TimeStretchCurve.cpp:182
FTimeStretchCurveInstance()
Definition TimeStretchCurve.h:248
float Get_T_Target() const
Definition TimeStretchCurve.h:300
const TArray< float > & GetMarkers_Original() const
Definition TimeStretchCurve.h:291
ENGINE_API float Convert_P_Original_To_Target(int32 InMarkerIndex, float In_P_Original) const
Definition TimeStretchCurve.cpp:464
ENGINE_API float Clamp_P_Target(float In_P_Target) const
Definition TimeStretchCurve.cpp:504
const TArray< float > & GetMarkers_Target() const
Definition TimeStretchCurve.h:294
ENGINE_API bool AreValidMarkerBookendsForPosition(float InPosition, float InP_CurrMarker, float InP_NextMarker) const
Definition TimeStretchCurve.cpp:411
float Get_T_Original() const
Definition TimeStretchCurve.h:297
bool HasValidData() const
Definition TimeStretchCurve.h:253
ENGINE_API void UpdateMarkerIndexForPosition(int32 &InOutMarkerIndex, float InPosition, const TArray< float > &InMarkerPositions) const
Definition TimeStretchCurve.cpp:378
ENGINE_API float Convert_P_Target_To_Original(int32 InMarkerIndex, float In_P_Target) const
Definition TimeStretchCurve.cpp:484
ENGINE_API int32 BinarySearchMarkerIndex(float InPosition, const TArray< float > &InMarkerPositions) const
Definition TimeStretchCurve.cpp:418
Definition TimeStretchCurve.h:22
FTimeStretchCurveMarker(float InT_Original, float InAlpha)
Definition TimeStretchCurve.h:39
Definition TimeStretchCurve.h:197
FTimeStretchCurve()
Definition TimeStretchCurve.h:203