|
| 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 | +} |
0 commit comments