Skip to content

Commit 4dccee7

Browse files
Post-Process Volume Curve Blending (#6188)
* Adding FadeDitherNode * Adding FadeDither documentation * Fixing extra newline * Changed DitherNode to FadeTransitionNode * Changing fade transition node docs. * First pass of curve blending. * Minor formatting fixes and comments * Removing unused variable * Fixing formatting on volume parameter. * Fixing formatting again. * Adding changes to changelog. * Changing curve code. * Cleaning up volume parameter * Working on volume parameter. * Fixing keyframes and adding tests. * Fixing GC in animation curve. * Fixing animation curve reset function. * Naming and documentation changes. * Changing keyframe interpolation to be more explicit about reusint the lhs value. * Fixing curve blend tests. * Moving unit test to core. * Update KeyframeUtility.cs * Moving curve blending interface. * Tweaking AnimationCurve blending. * Fixing Keyframe Blend GC allocation Co-authored-by: Sebastien Lagarde <[email protected]>
1 parent 0c5b28b commit 4dccee7

File tree

5 files changed

+482
-1
lines changed

5 files changed

+482
-1
lines changed

com.unity.render-pipelines.core/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
3131
- Added new DebugUI widget types: ProgressBarValue and ValueTuple
3232
- Added common support code for FSR.
3333
- Added new `RenderPipelineGlobalSettingsProvider` to help adding a settings panel for editing global settings.
34+
- Added blending for curves in post processing volumes.
3435

3536
## [13.1.0] - 2021-09-24
3637

Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Collections.ObjectModel;
4+
using System.Diagnostics;
5+
using System.Diagnostics.CodeAnalysis;
6+
using System.Linq;
7+
using System.Reflection;
8+
using Unity.Collections;
9+
using UnityEngine.Assertions;
10+
11+
namespace UnityEngine.Rendering
12+
{
13+
/// <summary>
14+
/// A helper function for interpolating AnimationCurves together. In general, curves can not be directly blended
15+
/// because they will have keypoints at different places. InterpAnimationCurve traverses through the keypoints.
16+
/// If both curves have a keypoint at the same time, they keypoints are trivially lerped together. However
17+
/// if one curve has a keypoint at a time that is missing in the other curve (which is the most common case),
18+
/// InterpAnimationCurve calculates a synthetic keypoint at that time based on value and derivative, and interpolates
19+
/// the resulting keys.
20+
///
21+
/// Note that this function should only be called by internal rendering code. It creates a small pool of animation
22+
/// curves and reuses them to avoid creating garbage. The number of curves needed is quite small, since curves only need
23+
/// to be used when interpolating multiple volumes together with different curve parameters. The underlying interp
24+
/// function isn't allowed to fail, so in the case where we run out of memory we fall back to returning a single keyframe.
25+
///
26+
/// <example>Example:
27+
/// <code>
28+
/// {
29+
/// AnimationCurve curve0 = new AnimationCurve();
30+
/// curve0.AddKey(new Keyframe(0.0f, 3.0f));
31+
/// curve0.AddKey(new Keyframe(4.0f, 2.0f));
32+
///
33+
/// AnimationCurve curve1 = new AnimationCurve();
34+
/// curve1.AddKey(new Keyframe(0.0f, 0.0f));
35+
/// curve1.AddKey(new Keyframe(2.0f, 1.0f));
36+
/// curve1.AddKey(new Keyframe(4.0f, 4.0f));
37+
///
38+
/// float t = 0.5f;
39+
/// KeyframeUtility.InterpAnimationCurve(curve0, curve1, t);
40+
///
41+
/// // curve0 now stores the resulting interpolated curve
42+
/// }
43+
/// </code>
44+
/// </example>
45+
46+
/// </summary>
47+
public class KeyframeUtility
48+
{
49+
/// <summary>
50+
/// Helper function to remove all control points for an animation curve. Since animation curves are reused in a pool,
51+
/// this function clears existing keys so the curve is ready for reuse.
52+
/// </summary>
53+
/// <param name="curve">The curve to reset.</param>
54+
static public void ResetAnimationCurve(AnimationCurve curve)
55+
{
56+
int numPoints = curve.length;
57+
for (int i = numPoints - 1; i >= 0; i--)
58+
{
59+
curve.RemoveKey(i);
60+
}
61+
}
62+
63+
static private Keyframe LerpSingleKeyframe(Keyframe lhs, Keyframe rhs, float t)
64+
{
65+
var ret = new Keyframe();
66+
67+
ret.time = Mathf.Lerp(lhs.time, rhs.time, t);
68+
ret.value = Mathf.Lerp(lhs.value, rhs.value, t);
69+
ret.inTangent = Mathf.Lerp(lhs.inTangent, rhs.inTangent, t);
70+
ret.outTangent = Mathf.Lerp(lhs.outTangent, rhs.outTangent, t);
71+
ret.inWeight = Mathf.Lerp(lhs.inWeight, rhs.inWeight, t);
72+
ret.outWeight = Mathf.Lerp(lhs.outWeight, rhs.outWeight, t);
73+
74+
// it's not possible to lerp the weightedMode, so use the lhs mode.
75+
ret.weightedMode = lhs.weightedMode;
76+
77+
// Note: ret.tangentMode is deprecated, so we will use the value from the constructor
78+
return ret;
79+
}
80+
81+
/// In an animation curve, the inTangent and outTangent don't match the edge of the curve. For example,
82+
/// the first key might have inTangent=3.0f but the actual incoming tangent is 0.0 because the curve is
83+
/// clamped outside the time domain. So this helper fetches a key, but zeroes out the inTangent of the first
84+
/// key and the outTangent of the last key.
85+
static private Keyframe GetKeyframeAndClampEdge([DisallowNull] NativeArray<Keyframe> keys, int index)
86+
{
87+
var lastKeyIndex = keys.Length - 1;
88+
if (index < 0 || index > lastKeyIndex)
89+
{
90+
Debug.LogWarning("Invalid index in GetKeyframeAndClampEdge. This is likely a bug.");
91+
return new Keyframe();
92+
}
93+
94+
var currKey = keys[index];
95+
if (index == 0)
96+
{
97+
currKey.inTangent = 0.0f;
98+
}
99+
if (index == lastKeyIndex)
100+
{
101+
currKey.outTangent = 0.0f;
102+
}
103+
return currKey;
104+
}
105+
106+
/// Fetch a key from the keys list. If index<0, then expand the first key backwards to startTime. If index>=keys.length,
107+
/// then extend the last key to endTime. Keys must be a valid array with at least one element.
108+
static private Keyframe FetchKeyFromIndexClampEdge([DisallowNull] NativeArray<Keyframe> keys, int index, float segmentStartTime, float segmentEndTime)
109+
{
110+
float startTime = Mathf.Min(segmentStartTime, keys[0].time);
111+
float endTime = Mathf.Max(segmentEndTime, keys[keys.Length - 1].time);
112+
113+
float startValue = keys[0].value;
114+
float endValue = keys[keys.Length - 1].value;
115+
116+
// In practice, we are lerping animcurves for post processing curves that are always clamping at the begining and the end,
117+
// so we are not implementing the other wrap modes like Loop, PingPong, etc.
118+
Keyframe ret;
119+
if (index < 0)
120+
{
121+
// when you are at a time either before the curve start time the value is clamped to the start time and the input tangent is ignored.
122+
ret = new Keyframe(startTime, startValue, 0.0f, 0.0f);
123+
}
124+
else if (index >= keys.Length)
125+
{
126+
// if we are after the end of the curve, there slope is always zero just like before the start of a curve
127+
var lastKey = keys[keys.Length - 1];
128+
ret = new Keyframe(endTime, endValue, 0.0f, 0.0f);
129+
}
130+
else
131+
{
132+
// only remaining case is that we have a proper index
133+
ret = GetKeyframeAndClampEdge(keys, index);
134+
}
135+
return ret;
136+
}
137+
138+
139+
/// Given a desiredTime, interpoloate between two keys to find the value and derivative. This function assumes that lhsKey.time <= desiredTime <= rhsKey.time,
140+
/// but will return a reasonable float value if that's not the case.
141+
static private void EvalCurveSegmentAndDeriv(out float dstValue, out float dstDeriv, Keyframe lhsKey, Keyframe rhsKey, float desiredTime)
142+
{
143+
// This is the same epsilon used internally
144+
const float epsilon = 0.0001f;
145+
146+
float currTime = Mathf.Clamp(desiredTime, lhsKey.time, rhsKey.time);
147+
148+
// (lhsKey.time <= rhsKey.time) should always be true. But theoretically, if garbage values get passed in, the value would
149+
// be clamped here to epsilon, and we would still end up with a reasonable value for dx.
150+
float dx = Mathf.Max(rhsKey.time - lhsKey.time, epsilon);
151+
float dy = rhsKey.value - lhsKey.value;
152+
float length = 1.0f / dx;
153+
float lengthSqr = length * length;
154+
155+
float m1 = lhsKey.outTangent;
156+
float m2 = rhsKey.inTangent;
157+
float d1 = m1 * dx;
158+
float d2 = m2 * dx;
159+
160+
// Note: The coeffecients are calculated to match what the editor does internally. These coeffeceients expect a
161+
// t in the range of [0,dx]. We could change the function to accept a range between [0,1], but then this logic would
162+
// be different from internal editor logic which could cause subtle bugs later.
163+
164+
float c0 = (d1 + d2 - dy - dy) * lengthSqr * length;
165+
float c1 = (dy + dy + dy - d1 - d1 - d2) * lengthSqr;
166+
float c2 = m1;
167+
float c3 = lhsKey.value;
168+
169+
float t = Mathf.Clamp(currTime - lhsKey.time, 0.0f, dx);
170+
171+
dstValue = (t * (t * (t * c0 + c1) + c2)) + c3;
172+
dstDeriv = (t * (3.0f * t * c0 + 2.0f * c1)) + c2;
173+
}
174+
175+
/// lhsIndex and rhsIndex are the indices in the keys array. The lhsIndex/rhsIndex may be -1, in which it creates a synthetic first key
176+
/// at startTime, or beyond the length of the array, in which case it creates a synthetic key at endTime.
177+
static private Keyframe EvalKeyAtTime([DisallowNull] NativeArray<Keyframe> keys, int lhsIndex, int rhsIndex, float startTime, float endTime, float currTime)
178+
{
179+
var lhsKey = KeyframeUtility.FetchKeyFromIndexClampEdge(keys, lhsIndex, startTime, endTime);
180+
var rhsKey = KeyframeUtility.FetchKeyFromIndexClampEdge(keys, rhsIndex, startTime, endTime);
181+
182+
float currValue;
183+
float currDeriv;
184+
KeyframeUtility.EvalCurveSegmentAndDeriv(out currValue, out currDeriv, lhsKey, rhsKey, currTime);
185+
186+
return new Keyframe(currTime, currValue, currDeriv, currDeriv);
187+
}
188+
189+
190+
/// <summary>
191+
/// Interpolates two AnimationCurves. Since both curves likely have control points at different places
192+
/// in the curve, this method will create a new curve from the union of times between both curves. However, to avoid creating
193+
/// garbage, this function will always replace the keys of lhsCurve with the final result, and return lhsCurve.
194+
/// </summary>
195+
/// <param name="lhsAndRetCurve">The start value. Additionaly, this instance will be reused and returned as the result.</param>
196+
/// <param name="rhsCurve">The end value.</param>
197+
/// <param name="t">The interpolation factor in range [0,1].</param>
198+
static public void InterpAnimationCurve(ref AnimationCurve lhsAndResultCurve, [DisallowNull] AnimationCurve rhsCurve, float t)
199+
{
200+
if (t <= 0.0f || rhsCurve.length == 0)
201+
{
202+
// no op. lhsAndResultCurve is already the result
203+
}
204+
else if (t >= 1.0f || lhsAndResultCurve.length == 0)
205+
{
206+
// In this case the obvous solution would be to return the rhsCurve. BUT (!) the lhsCurve and rhsCurve are different. This function is
207+
// called by:
208+
// stateParam.Interp(stateParam, toParam, interpFactor);
209+
//
210+
// stateParam (lhsCurve) is a temporary in/out parameter, but toParam (rhsCurve) might point to the original component, so it's unsafe to
211+
// change that data. Thus, we need to copy the keys from the rhsCurve to the lhsCurve instead of returning rhsCurve.
212+
KeyframeUtility.ResetAnimationCurve(lhsAndResultCurve);
213+
214+
for (int i = 0; i < rhsCurve.length; i++)
215+
{
216+
lhsAndResultCurve.AddKey(rhsCurve[i]);
217+
}
218+
lhsAndResultCurve.postWrapMode = rhsCurve.postWrapMode;
219+
lhsAndResultCurve.preWrapMode = rhsCurve.preWrapMode;
220+
}
221+
else
222+
{
223+
// Note: If we reached this code, we are guaranteed that both lhsCurve and rhsCurve are valid with at least 1 key
224+
225+
// create a native array for the temp keys to avoid GC
226+
var lhsCurveKeys = new NativeArray<Keyframe>(lhsAndResultCurve.length, Allocator.Temp);
227+
var rhsCurveKeys = new NativeArray<Keyframe>(rhsCurve.length, Allocator.Temp);
228+
229+
for (int i = 0; i < lhsAndResultCurve.length; i++)
230+
{
231+
lhsCurveKeys[i] = lhsAndResultCurve[i];
232+
}
233+
234+
for (int i = 0; i < rhsCurve.length; i++)
235+
{
236+
rhsCurveKeys[i] = rhsCurve[i];
237+
}
238+
239+
float startTime = Mathf.Min(lhsCurveKeys[0].time, rhsCurveKeys[0].time);
240+
float endTime = Mathf.Max(lhsCurveKeys[lhsAndResultCurve.length - 1].time, rhsCurveKeys[rhsCurve.length - 1].time);
241+
242+
// we don't know how many keys the resulting curve will have (because we will compact keys that are at the exact
243+
// same time), but in most cases we will need the worst case number of keys. So allocate the worst case.
244+
int maxNumKeys = lhsAndResultCurve.length + rhsCurve.length;
245+
int currNumKeys = 0;
246+
var dstKeys = new NativeArray<Keyframe>(maxNumKeys, Allocator.Temp);
247+
248+
int lhsKeyCurr = 0;
249+
int rhsKeyCurr = 0;
250+
251+
while (lhsKeyCurr < lhsCurveKeys.Length || rhsKeyCurr < rhsCurveKeys.Length)
252+
{
253+
// the index is considered invalid once it goes off the end of the array
254+
bool lhsValid = lhsKeyCurr < lhsCurveKeys.Length;
255+
bool rhsValid = rhsKeyCurr < rhsCurveKeys.Length;
256+
257+
// it's actually impossible for lhsKey/rhsKey to be uninitialized, but have to
258+
// add initialize here to prevent compiler erros
259+
var lhsKey = new Keyframe();
260+
var rhsKey = new Keyframe();
261+
if (lhsValid && rhsValid)
262+
{
263+
lhsKey = GetKeyframeAndClampEdge(lhsCurveKeys, lhsKeyCurr);
264+
rhsKey = GetKeyframeAndClampEdge(rhsCurveKeys, rhsKeyCurr);
265+
266+
if (lhsKey.time == rhsKey.time)
267+
{
268+
lhsKeyCurr++;
269+
rhsKeyCurr++;
270+
}
271+
else if (lhsKey.time < rhsKey.time)
272+
{
273+
// in this case:
274+
// rhsKey[curr-1].time <= lhsKey.time <= rhsKey[curr].time
275+
// so interpolate rhsKey at the lhsKey.time.
276+
rhsKey = KeyframeUtility.EvalKeyAtTime(rhsCurveKeys, rhsKeyCurr - 1, rhsKeyCurr, startTime, endTime, lhsKey.time);
277+
lhsKeyCurr++;
278+
}
279+
else
280+
{
281+
// only case left is (lhsKey.time > rhsKey.time)
282+
Assert.IsTrue(lhsKey.time > rhsKey.time);
283+
284+
// this is the reverse of the lhs key case
285+
// lhsKey[curr-1].time <= rhsKey.time <= lhsKey[curr].time
286+
// so interpolate lhsKey at the rhsKey.time.
287+
lhsKey = KeyframeUtility.EvalKeyAtTime(lhsCurveKeys, lhsKeyCurr - 1, lhsKeyCurr, startTime, endTime, rhsKey.time);
288+
rhsKeyCurr++;
289+
}
290+
}
291+
else if (lhsValid)
292+
{
293+
// we are still processing lhsKeys, but we are out of rhsKeys, so increment lhs and evaluate rhs
294+
lhsKey = GetKeyframeAndClampEdge(lhsCurveKeys, lhsKeyCurr);
295+
296+
// rhs will be evaluated between the last rhs key and the extrapolated rhs key at the end time
297+
rhsKey = KeyframeUtility.EvalKeyAtTime(rhsCurveKeys, rhsKeyCurr - 1, rhsKeyCurr, startTime, endTime, lhsKey.time);
298+
299+
lhsKeyCurr++;
300+
}
301+
else
302+
{
303+
// either lhsValid is True, rhsValid is True, or they are both True. So to miss the first two cases,
304+
// right here rhsValid must be true.
305+
Assert.IsTrue(rhsValid);
306+
307+
// we still have rhsKeys to lerp, but we are out of lhsKeys, to increment rhs and evaluate lhs
308+
rhsKey = GetKeyframeAndClampEdge(rhsCurveKeys, rhsKeyCurr);
309+
310+
// lhs will be evaluated between the last lhs key and the extrapolated lhs key at the end time
311+
lhsKey = KeyframeUtility.EvalKeyAtTime(lhsCurveKeys, lhsKeyCurr - 1, lhsKeyCurr, startTime, endTime, rhsKey.time);
312+
313+
rhsKeyCurr++;
314+
}
315+
316+
var dstKey = KeyframeUtility.LerpSingleKeyframe(lhsKey, rhsKey, t);
317+
dstKeys[currNumKeys] = dstKey;
318+
currNumKeys++;
319+
}
320+
321+
// Replace the keys in lhsAndResultCurve with our interpolated curve.
322+
KeyframeUtility.ResetAnimationCurve(lhsAndResultCurve);
323+
for (int i = 0; i < currNumKeys; i++)
324+
{
325+
lhsAndResultCurve.AddKey(dstKeys[i]);
326+
}
327+
328+
dstKeys.Dispose();
329+
}
330+
}
331+
}
332+
}

com.unity.render-pipelines.core/Runtime/Volume/KeyframeUtility.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

com.unity.render-pipelines.core/Runtime/Volume/VolumeParameter.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
using System.Diagnostics;
55
using System.Linq;
66
using System.Reflection;
7+
using Unity.Collections;
8+
using UnityEngine.Assertions;
79

810
namespace UnityEngine.Rendering
911
{
@@ -1649,7 +1651,21 @@ public class AnimationCurveParameter : VolumeParameter<AnimationCurve>
16491651
public AnimationCurveParameter(AnimationCurve value, bool overrideState = false)
16501652
: base(value, overrideState) { }
16511653

1652-
// TODO: Curve interpolation
1654+
/// <summary>
1655+
/// Interpolates between two AnimationCurve values. Note that it will overwrite the values in lhsCurve,
1656+
/// whereas rhsCurve data will be unchanged. Thus, it is legal to call it as:
1657+
/// stateParam.Interp(stateParam, toParam, interpFactor);
1658+
/// However, It should NOT be called when the lhsCurve parameter needs to be preserved. But the current
1659+
/// framework modifies it anyway in VolumeComponent.Override for all types of VolumeParameters
1660+
/// </summary>
1661+
/// <param name="lhsCurve">The start value.</param>
1662+
/// <param name="rhsCurve">The end value.</param>
1663+
/// <param name="t">The interpolation factor in range [0,1].</param>
1664+
public override void Interp(AnimationCurve lhsCurve, AnimationCurve rhsCurve, float t)
1665+
{
1666+
m_Value = lhsCurve;
1667+
KeyframeUtility.InterpAnimationCurve(ref m_Value, rhsCurve, t);
1668+
}
16531669
}
16541670

16551671
/// <summary>

0 commit comments

Comments
 (0)