Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Question Gradients, number of keys and Alpha values?

Discussion in 'Scripting' started by UlfvonEschlauer, Jan 27, 2022.

  1. UlfvonEschlauer

    UlfvonEschlauer

    Joined:
    Dec 3, 2014
    Posts:
    127
    Hey everyone!

    So I wanted to use Unity's gradient controls to control certain aspects of my volume renderer, but I am either doing something wrong, or I am running into some rather strange limitations.
    For one it seems like the number of keys is limited to 8? This is way too few for what I need. Is there a way to increase that number?
    Further it seems like that while I can get the Colors to be in floats, I can not get more than 255 levels for the Alpha.
    The Alpha levels might not be quite as bad, but 8 Gradient keys are waaaay too few.
    Please tell me that there is a way around those limits.
    Here is what I did to get the HDR for my gradient colors (the bool defines that). But it only seems to apply to the colors. I can not find an option to increase the number of keys?

    Code (CSharp):
    1.  
    2. gradientControls.OpacityAndColor = EditorGUILayout.GradientField(new GUIContent("OpacityAndColor"), gradientControls.OpacityAndColor, true);
    3.  
    Thanks a lot!
     
  2. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,070
    If you check out this page in the manual.
    It says "Color keys of the gradient (maximum 8 color keys)."

    You can make your own Gradient object, which isn't that hard. However, you then need to make your own GradientField control for the IMGUI which is arguably harder.

    If you really need this, a hybrid solution would be to make only a custom Gradient object, and ignore the IMGUI tool, and make a list of colors instead.

    To implement your own custom Gradient object, you need to understand that it does two things. One is to collect keys which contain { color, alpha } properties, and the other is to evaluate (interpolate) based on some parameter t (typically 0 .. 1). Gradient evaluators can be made much more fancy, but you seem to be okay with the basic linear interpolation.

    To make this, you have to imagine having keys sitting anywhere between 0 .. 1, so this is another property you need to account for: { t, color, alpha }. Then you have to be able to sort the keys based on t (ideally only when the list changes), to have an easier time finding the two neighboring ones for any arbitrary point on this segment.

    Imagine a simple diagram of a segment going from 0 to 1, and now imagine 3 keys, positioned at 0%, 75% and 85%

    Q1 What should a color at 50% be?
    Q2 What should a color at 80% be?
    Q3 What should a color at 90% be?

    Let's get you properly introduced to linear interpolation.

    On one hand, you want a value that's some % in between A and B.
    You basically mix the two values in some linear proportion.
    If you want them 50-50 you add half of A and half of B. You get an average, right?
    x = (a + b) * 0.5


    However, a generalized "mixture" looks like this
    x = a * (1 - t) + b * t


    Let's check this out, if A was 10, and B was 20
    This would make a t of 30% result in
    10 * (1 - 0.3) + 20 * 0.3 = 7 + 6 = 13

    Interesting right? You end up on a value exactly 3/10ths away from A, and 7/10ths away from B.
    This means that t is literally at 30% of this segment between A and B.

    An inverse concept is also important. If I told you we have A at 10 and B at 20 and some point X at 16, what's t equal to? Intuitively you can guess it's 60%, and it is, but let's do the actual inversion

    Code (csharp):
    1. a * (1 - t) + b * t = x
    2. b * t + a * (1 - t) = x
    3. b * t + a - a * t = x
    4. t * (b - a) + a = x
    5. t * (b - a) = x - a
    6. t = (x - a) / (b - a)
    So if A was 10, and B was 20, and X was 16
    (16 - 10) / (20 - 10) = 6 / 10 = 0.6


    We can now formally write some code
    Code (csharp):
    1. // lerp is short for linear interpolation
    2. float Lerp(float min, float max, float t) => min * (1f - t) + max * t;
    3. float InverseLerp(float min, float max, float v) => (v - min) / (max - min);
    Let's now return to questions.

    For Q1
    t (50%) lies between keys 0 (0%) and 1 (75%).
    Where would t be if we stretched this from 0 to 100%?
    This is answered by InverseLerp(0, 0.75, 0.5) = 0.6666
    So what's the evaluated color here?
    This is answered by Lerp(color1, color2, 0.6666) (note that this kind of lerp works with the colors)
    This works with alpha as well.

    For Q2
    t (80%) lies between keys 1 (75%) and 2 (85%).
    Where would t be if we stretched this from 0 to 100%?
    InverseLerp(0.75, 0.85, 0.8) = 0.5
    So what's the evaluated color here?
    This is answered by Lerp(color1, color2, 0.5)

    For Q3
    t (90%) lies to the right of the rightmost key 2 (85%).
    Because there are no other neighbors, we assume that this key's color is fixed.

    This was the hardest part.

    To find where input t lands exactly, you simply walk through the sorted keys in order, and accumulate them until the result surpasses it stop once a key's t surpasses the input t. This key and the previous key are the neighbors. If there are no more keys and input t is still greater, or when there was no previous key, this is a fixed color (non-interpolated).

    Code might look something like this (edit: check the next post for getNeighborKeys because I made a mistake)

    Code (csharp):
    1. public Color Evaluate(float t) {
    2.   if(_keys.Length == 0) return new Color(0f, 0f, 0f, 0f);
    3.   var n = getNeighborKeys(_keys, t);
    4.   if(n.l < 0) return _keys[n.r].Color;
    5.     else if(n.r < 0) return _keys[n.l].Color;
    6.   return Color.Lerp(_keys[n.l].Color, _keys[n.r].Color, n.t);
    7. }

    Feel free to ask if you need more detail.
     
    Last edited: Jan 29, 2022
  3. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,070
    In fact upon further inspection, you do not accumulate the t's in keys, because these values are absolute, not incremental. That's a mistake on my part.

    Code (csharp):
    1. // this internal function is made so that it returns three values through a tuple
    2. // this is particularly convenient in this case, because we want to get the left neighbor, the
    3. // right neighbor, and the actual interpolant
    4. (int l, int r, float t) getNeighborKeys(GradientKey[] keys, float t) {
    5.   for(int i = 0; i < keys.Length; i++) {
    6.     if(keys[i].t >= t) {
    7.       if(i == 0) return ( -1, 0, 0f ); // we use -1 to denote that the neighbor is missing
    8.       return ( i - 1, i, InverseLerp(keys[i - 1].t, keys[i].t, t) );
    9.     }
    10.   }
    11.   return ( keys.Length - 1, -1, 0f );
    12. }
    edit: typos, typos everywhere :)
     
    Last edited: Jan 29, 2022
  4. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,070
    Btw when it comes to lerping, Unity comes with basic lerps on its own.
    You can simply use Mathf.Lerp and Mathf.InverseLerp. Likewise, there is also Color.Lerp.
    Pay attention to Mathf.LerpUnclamped (and Color.LerpUnclamped) because this is the variant I have implemented above.

    Unity's (ordinary) Mathf.Lerp does this
    Code (csharp):
    1. public static float Lerp(float min, float max, float t) {
    2.   t = Mathf.Clamp01(t); // this prevents t from being less than 0 or greater than 1
    3.   return min * (1f - t) + max * t;
    4. }
    Color.Lerp thus does the following
    Code (csharp):
    1. public static Color Lerp(Color a, Color b, float t) {
    2.   t = Mathf.Clamp01(t);
    3.   return new Color(
    4.     Mathf.Lerp(a.r, b.r, t),
    5.     Mathf.Lerp(a.g, b.g, t),
    6.     Mathf.Lerp(a.b, b.b, t),
    7.     Mathf.Lerp(a.a, b.a, t)
    8.   );
    9. }
    It is also worth noting that InverseLerp works correctly when min and max are different values, as apparent by that formula
    Code (csharp):
    1. float InverseLerp(float min, float max, float v) => (v - min) / (max - min);
    If (max - min) evaluate to zero, we get division by zero (which might return NaN result). So you might want to handle that case differently. In reality, if the two values are very close to each other (or equal), the result should be any of the two, it doesn't matter which one if they are so close to each other.

    This is how you can handle this behavior for a more robust outcome
    Code (csharp):
    1. float InverseLerp(float min, float max, float v) {
    2.   var denom = max - min;
    3.   if(Mathf.Abs(denom) < 1E-7f) return min; // compare against a sufficiently small number for 32-bit precision
    4.   return (v - min) / denom;
    5. }
     
    Last edited: Jan 29, 2022
  5. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,070
    And so how do you actually assemble all of this into an actual gradient?

    Code (csharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public struct MyGradientKey {
    6.  
    7.   public float t { get; set; }
    8.   public Color Color { get; set; } // comes with r, g, b, alpha
    9.  
    10.   public MyGradientKey(float t, Color color) {
    11.     this.t = t;
    12.     this.Color = color;
    13.   }
    14.  
    15. }
    16.  
    17. public class MyGradient {
    18.  
    19.   List<MyGradientKey> _keys;
    20.  
    21.   public MyGradient() {
    22.     _keys = new List<MyGradientKey>();
    23.   }
    24.  
    25.   public int Count => _keys.Count;
    26.  
    27.   public MyGradientKey this[int index] {
    28.     get => _keys[index];
    29.     set { _keys[index] = value; sortKeys(); }
    30.   }
    31.  
    32.   public void AddKey(float t, Color color)
    33.     => AddKey(new MyGradientKey(t, color));
    34.  
    35.   public void AddKey(MyGradientKey key) {
    36.     _keys.Add(key);
    37.     sortKeys();
    38.   }
    39.  
    40.   public void InsertKey(int index, float t, Color color)
    41.     => InsertKey(index, new MyGradientKey(t, color));
    42.  
    43.   public void InsertKey(int index, MyGradientKey key) {
    44.     _keys.Insert(index, key);
    45.     sortKeys();
    46.   }
    47.  
    48.   public void RemoveKey(int index) {
    49.     _keys.RemoveAt(index);
    50.     sortKeys();
    51.   }
    52.  
    53.   public void RemoveInRange(float min, float max) {
    54.     for(int i = _keys.Count - 1; i >= 0; i--)
    55.       if(_keys[i].t >= min && _keys[i].t <= max) _keys.RemoveAt(i);
    56.     sortKeys();
    57.   }
    58.  
    59.   public void Clear() => _keys.Clear();
    60.  
    61.   void sortKeys() => _keys.Sort( (a, b) => a.t.CompareTo(b.t) );
    62.  
    63.   (int l, int r) getNeighborKeys(float t) {
    64.     var l = Count - 1;
    65.  
    66.     for(int i = 0; i <= l; i++) {
    67.       if(_keys[i].t >= t) {
    68.         if(i == 0) return ( -1, i );
    69.         return ( i - 1, i );
    70.       }
    71.     }
    72.  
    73.     return ( l, -1 );
    74.   }
    75.  
    76.   public Color Evaluate(float t) {
    77.     if(Count == 0) return new Color(0f, 0f, 0f, 0f);
    78.  
    79.     var n = getNeighborKeys(t);
    80.  
    81.     if(n.l < 0) return _keys[n.r].Color;
    82.       else if(n.r < 0) return _keys[n.l].Color;
    83.  
    84.     return Color.Lerp(
    85.       _keys[n.l].Color,
    86.       _keys[n.r].Color,
    87.       Mathf.InverseLerp(_keys[n.l].t, _keys[n.r].t, t)
    88.     );
    89.   }
    90.  
    91. }

    There.

    Edit: I've decided to simplify
    getNeighborKeys
    and compute interpolant in
    Evaluate
    instead. It looks and performs slightly better.
     
    Last edited: Jan 29, 2022
  6. UlfvonEschlauer

    UlfvonEschlauer

    Joined:
    Dec 3, 2014
    Posts:
    127
    Thanks for the very in depth reply!
    I will look into this once I get back to the gradients. Any idea why the Unity Devs chose to limit gradients to only 8 keys ( I mean I presume it is because it is a byte, but still)?
    It is seems like a pretty bad limitation.
     
  7. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,070
    I think it was an intentional design limit made with performance in mind. Or to make you aware that you should really do something of your own if you want anything more specialized than that. Or both.

    It's not because it's a byte though. 8 keys are structs, each having at least 4 bytes (32-bit floating point), one for each channel. Each gradient object thus has at most 32 bytes + overhead array info.