using System; using System.Runtime.CompilerServices; using UnityEngine.Experimental.Rendering; namespace UnityEngine.Rendering { // Due to limitations in the builtin AnimationCurve we need this custom wrapper. // Improvements: // - Dirty state handling so we know when a curve has changed or not // - Looping support (infinite curve) // - Zero-value curve // - Cheaper length property /// <summary> /// A wrapper around <c>AnimationCurve</c> to automatically bake it into a texture. /// </summary> [Serializable] public class TextureCurve : IDisposable { const int k_Precision = 128; // Edit LutBuilder3D if you change this value const float k_Step = 1f / k_Precision; /// <summary> /// The number of keys in the curve. /// </summary> [field: SerializeField] public int length { get; private set; } // Calling AnimationCurve.length is very slow, let's cache it [SerializeField] bool m_Loop; [SerializeField] float m_ZeroValue; [SerializeField] float m_Range; [SerializeField] AnimationCurve m_Curve; AnimationCurve m_LoopingCurve; Texture2D m_Texture; bool m_IsCurveDirty; bool m_IsTextureDirty; /// <summary> /// Retrieves the key at index. /// </summary> /// <param name="index">The index to look for.</param> /// <returns>A key.</returns> public Keyframe this[int index] => m_Curve[index]; /// <summary> /// Creates a new <see cref="TextureCurve"/> from an existing <c>AnimationCurve</c>. /// </summary> /// <param name="baseCurve">The source <c>AnimationCurve</c>.</param> /// <param name="zeroValue">The default value to use when the curve doesn't have any key.</param> /// <param name="loop">Should the curve automatically loop in the given <paramref name="bounds"/>?</param> /// <param name="bounds">The boundaries of the curve.</param> public TextureCurve(AnimationCurve baseCurve, float zeroValue, bool loop, in Vector2 bounds) : this(baseCurve.keys, zeroValue, loop, bounds) { } /// <summary> /// Creates a new <see cref="TextureCurve"/> from an arbitrary number of keyframes. /// </summary> /// <param name="keys">An array of Keyframes used to define the curve.</param> /// <param name="zeroValue">The default value to use when the curve doesn't have any key.</param> /// <param name="loop">Should the curve automatically loop in the given <paramref name="bounds"/>?</param> /// <param name="bounds">The boundaries of the curve.</param> public TextureCurve(Keyframe[] keys, float zeroValue, bool loop, in Vector2 bounds) { m_Curve = new AnimationCurve(keys); m_ZeroValue = zeroValue; m_Loop = loop; m_Range = bounds.magnitude; length = keys.Length; SetDirty(); } /// <summary> /// Finalizer. /// </summary> ~TextureCurve() { } /// <summary> /// Cleans up the internal texture resource. /// </summary> [Obsolete("Please use Release() instead.")] public void Dispose() { } /// <summary> /// Releases the internal texture resource. /// </summary> public void Release() { CoreUtils.Destroy(m_Texture); m_Texture = null; } /// <summary> /// Marks the curve as dirty to trigger a redraw of the texture the next time <see cref="GetTexture"/> /// is called. /// </summary> [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetDirty() { m_IsCurveDirty = true; m_IsTextureDirty = true; } static GraphicsFormat GetTextureFormat() { if (SystemInfo.IsFormatSupported(GraphicsFormat.R16_SFloat, FormatUsage.Sample | FormatUsage.SetPixels)) return GraphicsFormat.R16_SFloat; if (SystemInfo.IsFormatSupported(GraphicsFormat.R8_UNorm, FormatUsage.Sample | FormatUsage.SetPixels)) return GraphicsFormat.R8_UNorm; return GraphicsFormat.R8G8B8A8_UNorm; } /// <summary> /// Gets the texture representation of this curve. /// </summary> /// <returns>A 128x1 texture.</returns> public Texture2D GetTexture() { if (m_Texture == null) { m_Texture = new Texture2D(k_Precision, 1, GetTextureFormat(), TextureCreationFlags.None); m_Texture.name = "CurveTexture"; m_Texture.hideFlags = HideFlags.HideAndDontSave; m_Texture.filterMode = FilterMode.Bilinear; m_Texture.wrapMode = TextureWrapMode.Clamp; m_IsTextureDirty = true; } if (m_IsTextureDirty) { var pixels = new Color[k_Precision]; for (int i = 0; i < pixels.Length; i++) pixels[i].r = Evaluate(i * k_Step); m_Texture.SetPixels(pixels); m_Texture.Apply(false, false); m_IsTextureDirty = false; } return m_Texture; } /// <summary> /// Evaluate a time value on the curve. /// </summary> /// <param name="time">The time within the curve you want to evaluate.</param> /// <returns>The value of the curve, at the point in time specified.</returns> public float Evaluate(float time) { if (m_IsCurveDirty) length = m_Curve.length; if (length == 0) return m_ZeroValue; if (!m_Loop || length == 1) return m_Curve.Evaluate(time); if (m_IsCurveDirty) { if (m_LoopingCurve == null) m_LoopingCurve = new AnimationCurve(); var prev = m_Curve[length - 1]; prev.time -= m_Range; var next = m_Curve[0]; next.time += m_Range; m_LoopingCurve.keys = m_Curve.keys; // GC pressure m_LoopingCurve.AddKey(prev); m_LoopingCurve.AddKey(next); m_IsCurveDirty = false; } return m_LoopingCurve.Evaluate(time); } /// <summary> /// Adds a new key to the curve. /// </summary> /// <param name="time">The time at which to add the key.</param> /// <param name="value">The value for the key.</param> /// <returns>The index of the added key, or -1 if the key could not be added.</returns> [MethodImpl(MethodImplOptions.AggressiveInlining)] public int AddKey(float time, float value) { int r = m_Curve.AddKey(time, value); if (r > -1) SetDirty(); return r; } /// <summary> /// Removes the keyframe at <paramref name="index"/> and inserts <paramref name="key"/>. /// </summary> /// <param name="index"></param> /// <param name="key"></param> /// <returns>The index of the keyframe after moving it.</returns> [MethodImpl(MethodImplOptions.AggressiveInlining)] public int MoveKey(int index, in Keyframe key) { int r = m_Curve.MoveKey(index, key); SetDirty(); return r; } /// <summary> /// Removes a key. /// </summary> /// <param name="index">The index of the key to remove.</param> [MethodImpl(MethodImplOptions.AggressiveInlining)] public void RemoveKey(int index) { m_Curve.RemoveKey(index); SetDirty(); } /// <summary> /// Smoothes the in and out tangents of the keyframe at <paramref name="index"/>. A <paramref name="weight"/> of 0 evens out tangents. /// </summary> /// <param name="index">The index of the keyframe to be smoothed.</param> /// <param name="weight">The smoothing weight to apply to the keyframe's tangents.</param> [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SmoothTangents(int index, float weight) { m_Curve.SmoothTangents(index, weight); SetDirty(); } } /// <summary> /// A <see cref="VolumeParameter"/> that holds a <see cref="TextureCurve"/> value. /// </summary> [Serializable] public class TextureCurveParameter : VolumeParameter<TextureCurve> { /// <summary> /// Creates a new <see cref="TextureCurveParameter"/> instance. /// </summary> /// <param name="value">The initial value to store in the parameter.</param> /// <param name="overrideState">The initial override state for the parameter.</param> public TextureCurveParameter(TextureCurve value, bool overrideState = false) : base(value, overrideState) { } /// <summary> /// Release implementation. /// </summary> public override void Release() => m_Value.Release(); // TODO: TextureCurve interpolation } }