Нема описа
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

KeyframeUtility.cs 16KB

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