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