Lerping between two gradients with more than 8 keys

Hiya!

I need to lerp between two gradients that are used in a visual effect graph, which could have keys set in different places.

Is there a way to do this that can create a new lerped gradient with more than 8 keys?

I’ve found this very helpful script online, but it uses Gradient.SetKey, so only allows 8 keys to be set:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Util
{
    public static class Gradient
    {
        public static UnityEngine.Gradient Lerp(UnityEngine.Gradient a, UnityEngine.Gradient b, float t)
        {
            return Lerp(a, b, t, false, false);
        }

        public static UnityEngine.Gradient LerpNoAlpha(UnityEngine.Gradient a, UnityEngine.Gradient b, float t)
        {
            return Lerp(a, b, t, true, false);
        }

        public static UnityEngine.Gradient LerpNoColor(UnityEngine.Gradient a, UnityEngine.Gradient b, float t)
        {
            return Lerp(a, b, t, false, true);
        }

        static UnityEngine.Gradient Lerp(UnityEngine.Gradient a, UnityEngine.Gradient b, float t, bool noAlpha, bool noColor)
        {
            //list of all the unique key times
            var keysTimes = new List<float>();

            if (!noColor)
            {
                for (int i = 0; i < a.colorKeys.Length; i++)
                {
                    float k = a.colorKeys[i].time;
                    if (!keysTimes.Contains(k))
                        keysTimes.Add(k);
                }

                for (int i = 0; i < b.colorKeys.Length; i++)
                {
                    float k = b.colorKeys[i].time;
                    if (!keysTimes.Contains(k))
                        keysTimes.Add(k);
                }
            }

            if (!noAlpha)
            {
                for (int i = 0; i < a.alphaKeys.Length; i++)
                {
                    float k = a.alphaKeys[i].time;
                    if (!keysTimes.Contains(k))
                        keysTimes.Add(k);
                }

                for (int i = 0; i < b.alphaKeys.Length; i++)
                {
                    float k = b.alphaKeys[i].time;
                    if (!keysTimes.Contains(k))
                        keysTimes.Add(k);
                }
            }

            GradientColorKey[] clrs = new GradientColorKey[keysTimes.Count];
            GradientAlphaKey[] alphas = new GradientAlphaKey[keysTimes.Count];

            //Pick colors of both gradients at key times and lerp them
            for (int i = 0; i < keysTimes.Count; i++)
            {
                float key = keysTimes[i];
                var clr = Color.Lerp(a.Evaluate(key), b.Evaluate(key), t);
                clrs[i] = new GradientColorKey(clr, key);
                alphas[i] = new GradientAlphaKey(clr.a, key);
            }

            var g = new UnityEngine.Gradient();
            g.SetKeys(clrs, alphas);

            return g;
        }
    }
}

I read somewhere else that I could create a Color array of all the points of the gradient and then lerp these, but I’m unsure how I convert this array back to a new gradient?

    [GradientUsage(true)]
    public Gradient Gradient_Skinned;
    [ColorUsage(true, true)]
    public Color[] GradientValues_Skinned;

    void CalculateGradientValues()
    {
        GradientValues_Skinned = new Color[256];
        for (int i = 0; i < 256; i++)
        {
            GradientValues_Skinned[i] = Gradient_Skinned.Evaluate((float)i / 255f);
        }
    }

Am I missing something?

Unsure if I can only work with gradients with 4 keys (or 8 in the exact same position perhaps), or if there’s another solution? Thanks!

Could you explain what you mean by “Lerp between two gradients”?

2 Likes

I have a class for gradients without the limit. I’ve stripped it down a bit for simplicity (hopefully without breaking it in the process), maybe you can use it directly or it’ll be useful to someone else:

using UnityEngine;
public class GradientPlus
{
  public Color[] colors;
  public float[] colorPositions;
  public static GradientPlus Create(int colorCount)
  {
    GradientPlus instance = new GradientPlus();
    instance.colors = new Color[colorCount];
    instance.colorPositions = new float[colorCount];
    return instance;
  }
  public void SetColor(int index, float position, float r, float g, float b, float a)
  {
    colors[index] = new Color(r, g, b, a);
    colorPositions[index] = position;
  }
  public Color Evaluate(float position)
  {
    int indexFrom = 0;
    int indexTo = 1;
    position = Mathf.Clamp(position, 0, 1);
    for (int i = 0; i < (colors.Length - 1); i++)
    {
      if ((position >= colorPositions[i]) && (position <= colorPositions[i + 1]))
      {
        indexFrom = i;
        indexTo = i + 1;
        break;
      }
    }
    if (indexFrom == indexTo) return colors[indexTo];

    float proSpan = colorPositions[indexTo] - colorPositions[indexFrom];
    float pro = (position - colorPositions[indexFrom]) / proSpan;
    return Color.Lerp(colors[indexFrom], colors[indexTo], pro);
  }
}

You use it like this:

// create a gradient with however many values that you want:
GradientPlus g = GradientPlus.Create(3); // this example red->yellow->green

// populate the values: index, position, r, g, b, a
g.SetColor(0, 0, 1, 0, 0, 1);
g.SetColor(1, 0.5f, 1, 1, 0, 1);
g.SetColor(2, 1, 0, 1, 0, 1);

// Evaluate works same as usual (0-1, where 0.5f is half way between start and end)
Color atHalfWay = g.Evaluate(0.5f);

Edit: simplified to lerp

Sorry, I’m bad at explaining!

I have a visual effect which uses a gradient to set the colour over lifetime.
6448704--721992--Capture4.PNG

I also have a scriptable object which has different sets of values for all the visual effect properties.

6448704--721995--Capture5.PNG

On a certain input, I want the visual effect properties to blend from their current values to the stored values from the scriptable object, over x amount of seconds.

This is fine for floats and colours, but I’m unsure of how to blend one gradient to another over x amount of seconds.

I hope that makes sense!

you sample from both gradients, then lerp between the results.
obviously this will get you a single color.

if you want to blend two gradients in their entirety, you need to fully commit to creating a new gradient that takes all the keys into account from both source gradients, while blending them together.

i.e. try imagining gradients as translucent strips, and superimposing one on top of another. this would be a 50% full gradient blend.

this requires a bit of logic on your part. when you move from 0 to 1, some keys might exist in one gradient, while missing on the other, and vice versa. thus you need to abstract sampling, whilst keeping the order of keyed changes in place. in return, your resulting gradient needs to have all the keys that are visible when you superimpose the gradients like this (i.e. if G1 had 2 keys, and G2 had 5 keys, the resulting gradient will have 7 keys minus the overlapping ones, most typically the keys on the ends, thus you’ll end up with 5). then you go through all the keys, and do what I said in the first sentence: sample from G1 and G2 at that location and apply color lerp.

That looks great - I’m struggling to work out how to use it in this case, as I can only see how to use the unity gradient for a visual effect graph. It looks like I’m probably going about this in the wrong way!

It’s probably me going about this in the wrong way, given you’re looking for a visual effect graph solution. I don’t really have any experience with that and don’t know how easy it is to integrate code (or if that’s even possible).

in steps

define some resolution (say 16)
create a variable that is a reciprocal of this (i.e. 1/16)
this is how many steps you intend to be able to detect, letting you discern between overlaps and non-overlaps

make a hashset based on ints, this is your collection of keys

scan through G1 color keys
convert its time value to a nearest representative integer: Mathf.FloorToInt(time * resolution)
save this to hashset

scan through G2 color keys
convert its time value to a nearest representative integer: Mathf.FloorToInt(time * resolution)
save this to hashset

at this point you have only unique representative integers in your hashset

manually convert your hashset to a list of floats, by iterating through it element by element
add to list each the following value: (float)integer * reciprocal (so it gets fixed back to 0…1 range)

sort this list in an ascending order

create a new gradient
iterate through all values in the list, and for each value:

  • sample both G1 and G2 at this time value and lerp the results
  • create new key with the result

return this gradient

increase resolution to get more truthful representation of the original gradients, changing this lets you have more control over the speed/precision trade off of the algorithm (better resolution means slower)

take this only as pseudocode (though it might just work out of the box)

using System.Collections.Generic;
using UnityEngine;

static public class GradientUtil {

  static public Gradient BlendTwoGradients(Gradient g1, Gradient g2, float lerp = 0.5f, int resolution = 64) {
    float reciprocal = 1f / resolution;
    var keys = new HashSet<int>();

    // add start and end keys
    keys.Add(0);
    keys.Add(resolution - 1);

    // sample the two gradients' time values
    for(int i = 0; i < g1.colorKeys.Length; i++)
      keys.Add(Mathf.FloorToInt(g1.colorKeys[i].time * resolution));

    for(int i = 0; i < g2.colorKeys.Length; i++)
      keys.Add(Mathf.FloorToInt(g2.colorKeys[i].time * resolution));

    // make a new list and populate this with unique renormalized time values
    var list = new List<float>(keys.Count);
    foreach(var key in keys) { list.Add((float)key * reciprocal); }

    // sort in ascending order
    list.Sort();

    var finalColorKeys = new GradientColorKey[list.Count];
    var finalAlphaKeys = new GradientAlphaKey[list.Count];

    // sample the two gradients and evaluate the actual blend of two colors
    for(int i = 0; i < list.Count; i++) {
      var color = Color.Lerp(g1.Evaluate(list[i]), g2.Evaluate(list[i]), lerp);
      finalColorKeys[i].color = new Color(color.r, color.g, color.b);
      finalColorKeys[i].time = list[i];
      finalAlphaKeys[i].alpha = color.a;
      finalAlphaKeys[i].time = list[i];
    }

    // create the new gradient and return it
    var gradient = new Gradient();
    gradient.SetKeys(finalColorKeys, finalAlphaKeys);
    return gradient;
  }

}

edit: forgot to fill the keys correctly
edit2: I recommend the code posted in #10 below, as this one contains some minor bugs and isn’t well-suited for per-frame animation

list.Sort() can be avoided for improved performance but sadly C# doesn’t include system bubble sort methods, and I don’t want to bloat this code if I expect anyone to understand what’s going on.

the point of resolution is basically to allow you to dynamically get rid of any redundant keys in an efficient manner; imagine if you had two very similar gradients, but one had a key at 0.4855 and the other was defined as 0.49. the resulting gradient should not contain both because the difference is irrelevant from your vantage point. this code will intentionally auto-assimilate any such differences, but a greater resolution means it will potentially leave a greater number of them. on the other hand, having too small resolution, say 2, will practically ignore anything except start and end (the keys will be skipped, so expect a bad gradient with bad colors).

even though you can specify resolution less than 2, the resulting gradient will always have two keys at a minimum.

it’s not terribly efficient to do this in every frame however (as it makes decent garbage, which is a jargon regarding memory consumption), so consider lowering the resolution if that’s the case.

if you intend to use this exclusively for animating per frame, then it can be made significantly faster, by computing the gradient structure once, and blending the colors in every frame, which will improve the behavior drastically, with less garbage and better performance overall. tell me if that’s the case, and I’ll try to change the design.

(edit: this version is buggy and incomplete in some respects, see post #17 for an improved version)

In fact, here you go.

Click for code

using System.Collections.Generic;
using UnityEngine;

public class GradientBlend {

  Gradient _g1;
  public Gradient gradient1 => _g1;

  Gradient _g2;
  public Gradient gradient2 => _g2;

  Gradient _blend;
  public Gradient blend => _blend;

  public int Resolution { get; }

  float _blendValue;
  public float CurrentBlend {
    get => _blendValue;
    set => UpdateBlend(value);
  }

  public Color Evaluate(float time) => _blend.Evaluate(time);
  public Color Evaluate(float time, float blend) {
    UpdateBlend(blend);
    return Evaluate(time);
  }

  List<float> _timeStamps;

  public GradientBlend(Gradient g1, Gradient g2, float blend = 0.5f, int resolution = 64) {
    _g1 = g1;
    _g2 = g2;
    Resolution = resolution;
    _blend = buildGradient();
    UpdateBlend(blend);
  }

  private void scanForKeysInColors(GradientColorKey[] keys, ref HashSet<int> keySet) {
    for(int i = 0; i < keys.Length; i++)
      keySet.Add(Mathf.FloorToInt(keys[i].time * Resolution));
  }

  private void scanForKeysInAlphas(GradientAlphaKey[] keys, ref HashSet<int> keySet) {
    for(int i = 0; i < keys.Length; i++)
      keySet.Add(Mathf.FloorToInt(keys[i].time * Resolution));
  }

  private List<float> buildTimeStamps(HashSet<int> keySet) {
    float reciprocal = 1f / Resolution;
    var list = new List<float>(keySet.Count);
    foreach(var key in keySet) { list.Add((float)key * reciprocal); }
    list.Sort();
    return list;
  }

  private Gradient buildGradient() {
    var keys = new HashSet<int>();
    keys.Add(0);
    keys.Add(resolution);

    scanForKeysInColors(_g1.colorKeys, ref keys);
    scanForKeysInColors(_g2.colorKeys, ref keys);
    scanForKeysInAlphas(_g1.alphaKeys, ref keys);
    scanForKeysInAlphas(_g2.alphaKeys, ref keys);

    _timeStamps = buildTimeStamps(keys);

    var gradient = new Gradient();
    gradient.SetKeys(
      new GradientColorKey[_timeStamps.Count],
      new GradientAlphaKey[_timeStamps.Count]
    );

    return gradient;
  }

  public void UpdateBlend(float blend) {
    for(int i = 0; i < _timeStamps.Count; i++) {
      var time = _timeStamps[i];
      var color = Color.Lerp(_g1.Evaluate(time), _g2.Evaluate(time), blend);
      _blend.colorKeys[i].color = new Color(color.r, color.g, color.b);
      _blend.alphaKeys[i].alpha = color.a;
      _blend.colorKeys[i].time = _blend.alphaKeys[i].time = time;
    }

    _blendValue = blend;
  }

}

Be mindful that you’re not supposed to change the original gradients after passing them to this object.
If you want this, make duplicates that won’t change, or make a new GradientBlend object each time (which defeats the purpose so do it only once in a while).

Normally it’s used like this (I don’t know how you can use it in the visual thingie graph, never used it myself)

var myBlend = new GradientBlend(myGradient1, myGradient2, 0.5f, 64); // you can omit the last two arguments to fall back to defaults
myBlend.UpdateBlend(0.6f); // to compute the actual gradient blend
myBlend.Evaluate(0.5f); // to get the color at 50% time

// alternatively you may ask for a color in any specific blend, as a compound query
myBlend.Evaluate(0.5f, 0.7f); // get the color at 50% time from the 70% blend

// you can also get to the following internal parameters through it
myBlend.gradient1; // returns the original gradient 1 (do not change its data)
myBlend.gradient2; // returns the original gradient 2 (do not change its data)
myBlend.blend; // returns the reference to the resulting blend gradient (do not change its data)
myBlend.Resolution; // returns the resolution value (read-only)
myBlend.CurrentBlend; // returns the last used blending value (you can also set it)

UpdateBlend is now safe to use in every frame of the animation.
I haven’t tested any of this, so feel free to scream at me if it doesn’t work.

edit:
I’ve polished it a little, and added support for any independently set alpha keys
also fixed a slight issue with how I treated the end key

Wow thanks so much for this - this is incredibly helpful! This level of C# is a bit beyond what I usually do, so I’m going through your comments much more slowly than you’re posting updates :slight_smile:

Your first piece of code works perfectly! I’m just working through your new comments now ( & looking up a lot of new terms to me) Thank you :slight_smile:

You’re welcome. I’m glad that it works, considering that my IDE is a forum edit box.

Just a few side points if anyone cares about this sort of stuff.

Why am I multiplying time values and then returning those values back into 0…1 range?
Instead of comparing individual time values to each other, which is really expensive in terms of computational time, we splice the gradient bar into small imaginary segments, like buckets, and as many of them as specified by resolution. This is particularly useful to avoid having to rely on computing differences between floating point values, which can be imprecise (i.e. we would never get zeroes even if they matched, but some really small numbers, which boils down to messy and sluggish code). Then it’s easy to just decide if a key belongs to this or that bucket.

For example, if a resolution was set to 3, that means there are 3 buckets per gradient. If there was a key at 38% and another at 72% we can tell to which of the buckets they belong. Floor(0.38 * 3) = 1 and Floor(0.72 * 3) = 2, so it’s bucket #2 and bucket #3 (remember we’re counting indices from zero).

Normally, we’d set resolution to some larger number, for example 64, so that individual differences can be as small as 1/64, or 0.015625. This is still a fine-looking gradient for all human eyeball purposes, and we’ve efficiently discarded all unnecessary data.

Why do I use a HashSet, instead of using a List or something similar?
HashSets are, like Dictionaries, very very fast in certain situations.
And they can contain only unique values, by their specification.

In this particular situation we want to store bucket integers in a collection, but only if there wasn’t the same value already. So if you store “12” twice, you won’t end up with two 12s. In this fashion, all duplicates are effectively ignored.

This would fail if you were to attempt to store these values as floating points, because 12.0 and 12.00001 would not register as being the same, but they kind of are!

And that kind of matters because we don’t care about the duplicates. This is the greatest benefit of all hash maps, HashSets included, for being able to find an element almost immediately, regardless of how many elements there are, instead of having to go through each element, and check them one by one, as is the case with lists and arrays.

But this introduces the next problem.

Why do I end up using a list that has to be sorted?
HashSets are, by specification, unordered sets. There is no guaranteed order once values are stored in this collection, but we have pulled them from gradients in some very specific order and now we have to put them back without breaking anything. Thankfully, we can reliably use the time values themselves to tell the exact order.

When used without arguments, List.Sort() sorts all elements in the ascending order by default. So we can populate the list using the foreach loop (notice how there is no index accessor, because there are no indices in this set; foreach is the only way to access the stored elements of a HashSet), and simply sort it in the end.

Bubble sort, a technique I mentioned earlier, would enable us to do this in one pass, which is only negligibly faster (and in some cases even slightly slower) than postprocessing Sort(); in my second variant of the code, this doesn’t improve the performance that much considering the work involved.

Why do I compute a reciprocal value beforehand instead of just dividing?
It’s just a good practice to pull any such divisions out from a loop. There is no need to divide in each iteration, as this is consistently slower on all platforms than just multiplying, and by a factor of 2-6x. Normally, the compiler will know what to do in such cases and will likely cache the value if it’s frequently used, and multiply with it anyways, but it’s still a good practice IF this practice doesn’t turn code into an unreadable mess.

That’s interesting to learn about HashSets, I’ll use them more in future!

I’m finding with the second set of scripts that the blended gradient is blank for some reason. I’m able to return gradient 1 or 2 correctly, but when returning a new blended gradient, there are no keys in it, except for key 0?

Not sure if I’ve called it incorrectly?

public class GradientBlending : MonoBehaviour
{
    public VisualEffect vfx;
    public Gradient grad1;
    public Gradient grad2;
    public Gradient blendGrad;
    public GradientBlend myBlend;

    private void Start()
    {
        myBlend = new GradientBlend(grad1, grad2);
    }

    private float t = 0;
    private float timeToLerp = 10.0f;

    void Update()
    {
        if (t >= timeToLerp)
           return;


        t += Time.deltaTime;

        if (t > timeToLerp)
            t = timeToLerp;

        float lerpAmount = t / timeToLerp;

        myBlend.UpdateBlend(lerpAmount);
        blendGrad = myBlend.blend;

        vfx.SetGradient("Colour_MainGradient", blendGrad);
    }

}

As an aside - for anyone else reading, this does have the potential to throw up the error that more than 8 colorkeys have been created, as Unity only allows for 8:

(I’ve found it useful as it blends keys that are close together)

Maybe I’m missing something here but why wouldn’t you just do something like this and call it a day? (In fact you’re using this concept to blend the keys in your first post)

// Bias is the weighting between gradients A and B. 0 =  full weight to A, 1 =  full weight to B.
Color BlendGradients(Gradient a, Gradient b, float bias, float t) {
  Color fromA = a.Evaluate(t);
  Color fromB = b.Evaluate(t);

  Color final = Color.Lerp(fromA, fromB, bias);
  return final;
}

Sorry if I’m missing something important. is it because you need to pass a single gradient into the VFX? Couldn’t you also just do this calculation inside the VFX graph itself?

@PraetorBlue
It is that, but she also needs the entire gradient blended as such.

@laurienash
Oh, thanks for the heads up. I’ll fix it asap.
Can you believe that I’ve never noticed that limit of 8 keys?? I’ll fix that as well.

I’ve fixed the issues. Also this time I made a (light) test to make sure it’s working properly.

using System.Collections.Generic;
using UnityEngine;

public class GradientBlend {

  private const int MAX_KEYS = 8;

  Gradient _g1;
  public Gradient gradient1 => _g1;

  Gradient _g2;
  public Gradient gradient2 => _g2;

  Gradient _blend;
  public Gradient blend => _blend;

  public int Resolution { get; private set; }

  bool _uniform;
  public bool UniformSampling {
    get => _uniform;
    set => Rebuild(_g1, _g2, _blendValue, value, Resolution);
  }

  float _blendValue;
  public float CurrentBlend {
    get => _blendValue;
    set => UpdateBlend(value);
  }

  public Color Evaluate(float time) => _blend.Evaluate(time);
  public Color Evaluate(float time, float blend) {
    UpdateBlend(blend);
    return Evaluate(time);
  }

  List<float> _timeStamps;

  public GradientBlend(Gradient g1, Gradient g2, float defaultBlend = .5f, bool uniformSampling = false, int resolution = 64) {
    Rebuild(g1, g2, defaultBlend, uniformSampling, resolution);
  }

  public void Rebuild(Gradient g1, Gradient g2, float defaultBlend = .5f, bool uniformSampling = false, int resolution = 64) {
    _g1 = g1;
    _g2 = g2;
    _uniform = uniformSampling;
    Resolution = uniformSampling? (MAX_KEYS - 1) : resolution;
    _blend = buildCombinedGradient();
    UpdateBlend(defaultBlend);
  }

  private void scanForKeysInColors(GradientColorKey[] keys, ref HashSet<int> keySet) {
    for(int i = 0; i < keys.Length; i++)
      keySet.Add(Mathf.FloorToInt(keys[i].time * Resolution));
  }

  private void scanForKeysInAlphas(GradientAlphaKey[] keys, ref HashSet<int> keySet) {
    for(int i = 0; i < keys.Length; i++)
      keySet.Add(Mathf.FloorToInt(keys[i].time * Resolution));
  }

  private List<float> buildTimeStamps(HashSet<int> keySet) {
    float reciprocal = 1f / Resolution;
    var list = new List<float>(keySet.Count);
    foreach(var key in keySet) { list.Add((float)key * reciprocal); }
    list.Sort();
    while(list.Count >= MAX_KEYS) list.RemoveAt(list.Count - 1);
    return list;
  }

  private Gradient buildCombinedGradient() {
    var keys = new HashSet<int>();

    if(!_uniform) {
      keys.Add(0);
      keys.Add(Resolution);

      scanForKeysInColors(_g1.colorKeys, ref keys);
      scanForKeysInColors(_g2.colorKeys, ref keys);
      scanForKeysInAlphas(_g1.alphaKeys, ref keys);
      scanForKeysInAlphas(_g2.alphaKeys, ref keys);

    } else {
      for(int i = 0; i < MAX_KEYS; i++) keys.Add(i);

    }

    _timeStamps = buildTimeStamps(keys);

    var gradient = new Gradient();
    gradient.SetKeys(
      new GradientColorKey[Mathf.Min(MAX_KEYS, _timeStamps.Count)],
      new GradientAlphaKey[Mathf.Min(MAX_KEYS, _timeStamps.Count)]
    );

    return gradient;
  }

  public void UpdateBlend(float blend) {
    var colorKeys = _blend.colorKeys;
    var alphaKeys = _blend.alphaKeys;

    for(int i = 0; i < _timeStamps.Count; i++) {
      var time = _timeStamps[i];
      var color = Color.Lerp(_g1.Evaluate(time), _g2.Evaluate(time), blend);
      colorKeys[i].color = new Color(color.r, color.g, color.b);
      alphaKeys[i].alpha = color.a;
      colorKeys[i].time = alphaKeys[i].time = time;
    }

    _blend.SetKeys(colorKeys, alphaKeys);
    _blendValue = blend;
  }

}

The main issue why it was blank was the internal design of the gradients which isn’t the happiest ever. Unity does some things behind the scenes, including re-sorting key arrays, so having to call SetKeys in the end is pretty much mandatory, which is really silly and shouldn’t work like that at all. Also the limit… oof, such a poor form, but I guess they have to guard themselves against noobs yelling how slow Unity is, when in fact, it’s them stomping it with megagradients :slight_smile:

Okay, so now the class won’t crash if your resulting gradient has more than 8 keys overall, but this introduces a relatively complex problem of having to decide which keys should be preserved, and which should be abandoned. There are many solutions to this problem.

In the end I’ve decided to prioritize the leftmost keys, which isn’t really nice because it can simply lose entire colors once the limit has been reached, BUT you can usually tweak (lower) the resolution to work around this.

Additionally, I’ve included another mode of sampling, called ‘uniform sampling’ where exactly 8 keys will be distributed uniformly along the gradient, trying to capture the blend without any biases, but mostly avoiding having to swallow colors that are beyond the limit (technically it will most likely swallow something else in the middle, so it’s a trade off; on the upside, it is faster).

Of course, this mode will give poor results with simpler gradients (with mid-keys, pure ramps are fine) or with gradients that have pronounced color peaks and changes, and it is to be used only when you’re using two relatively mild gradients which have too many keys to begin with.

That said, in addition to previous stuff, you now have access to this bool property, called UniformSampling. Constructor is changed to allow this setting as well.

public GradientBlend(Gradient g1, Gradient g2, float defaultBlend = .5f, bool uniformSampling = false, int resolution = 64)

Note: If you use uniform sampling, resolution argument will be ignored.
Changing the UniformSampling property at any point forces rebuilding of the blend gradient. (Rebuilding is much more expensive than just doing UpdateBlend.)

You can now also rebuild the gradient forcefully by calling Rebuild(...), allowing you to change the source gradients and other parameters, without having to create a new object.

Note: If you store myGradientBlend.blend reference to another variable, and then call Rebuild(...) (or change UniformSampling), your variable will remain pointing to a gradient that is no longer in use by GradientBlend (but is valid in itself). I’ve decided not to assign it to null, because you could as well store this gradient as a final product, change GradientBlend parameters, and repurpose it to produce another gradient. So who am I to judge, if you’re going to use the code as a gradient factory, so be it. Just don’t let this confuse you.

Rebuild method uses the exact same signature as constructor shown above.

I think that’s all there is to it.
Happy gamedev!

1 Like

Maybe someone could use it. Interaction in unity with Gradient via jobs + burst.
Also access Gradient.colourKeys and Gradient.alphaKey via NativeArray without a Garbage Collector.
Here’s how I did it.