llorens.marti.works

Procedural Gradients

Recently, I've been working on a part of a project where I needed to create procedural skyboxes. In the process of doing that I needed to generate gradients of colors that were coherent and mostly pleasant to the eyes.

The first idea and probably the most simple is to select a set of random colors where each color is created by randomly assigning a value between 0.0 and 1.0 on each channel. The end result is really bad because the colors don't have any consistency between them. Let see an example:

Randomly generated colors.

As we can see in the image above, randomly selecting colors is not the best approach for a gradient. Random colors tend to change without control and often produce combinations that are not pleasant to the eyes.

What I wanted was something more like the image bellowe, where gradients are created with colors that are more coherent and pleasant to see:

Better gradients :)

While there might be a lot of different ways to procedurally select colors for a gradient that are coherent and pleasant to the eyes, I am going to explain a process that I found on an empirical way.

Phase 1 - Selecting coherent base colors

Probably you are familiar with the concept of the Color Wheel, (if not, you can check it here). We are going to use the color wheel to help us select an initial list of colors that can be more coherent and visually pleasant than random ones.

Color wheel.

Initially we are going to select a list of 4 colors that will be the base colors of our procedural gradient. So, let's init a list of colors with the colors of the Color Wheel:

Color[] cw = new Color[6] {
    new Color(1, 0, 0, 1),
    new Color(1, 1, 0, 1),
    new Color(0, 1, 0, 1),
    new Color(0, 1, 1, 1),
    new Color(0, 0, 1, 1),
    new Color(1, 0, 1, 1),
};

We need to pick the first color as a random pick from the 6 colors of the Color Wheel. So, we pick a random index from 0 to 5 and use it as the first color.

int iIndex = Random.Range(0, 6);

The next colors need to be contiguous to the previous one, but for adding a bit more of randomness, we are going to pick the same color or the previous color with a lower probability (10% probability to be the same color, and 10% probability to be the previous one).

    int[] directions = new int[10] {-1, 0, 1, 1, 1, 1, 1, 1, 1, 1};

Also, we need to randomly select which is the direction that we are going to follow on the Color Wheel to pick the next color.

    int d = (Random.Range(0.0f, 1.0f) > 0.5f) ? 1 : -1;

With this, now we can pick the 4 base colors that our procedural gradient will be based on:

    int countPhase1 = 4;
    int curIdx = iIndex;
    Color[] cPhase1 = new Color[countPhase1];

    for (int i = 0; i < countPhase1; i++) {
        cPhase1[i] = cw[curIdx];
        curIdx = curIdx + (directions[Random.Range(0, 10)] * d);

        if (curIdx < 0) {curIdx = 5;}
        if (curIdx > 5) {curIdx = 0;}
    }

With this algorithm, we will end up with 4 colors that follow this characteristics:

As an example:

Coherent and contiguous colors.

To end up with Phase 1, we only need to load these 4 colors into a structure that allows us to sample intermediate colors. In this case, I am going to use the structure Gradient (more info here) that is provided by Unity. Keep in mind that these kind of structures allow you to sample an intermediate color given a value between 0.0 and 1.0.

So, if you have something like this:

(1, 0, 0, 1) 0.0
(1, 1, 0, 1) 0.25
(0, 1, 0, 1) 0.50
(0, 1, 1, 1) 0.75
(0, 0, 1, 1) 1.0

And you want to sample at position 0.625, you will get the color value that is exactly between (0, 1, 0, 1) and (0, 1, 1, 1), which is (0, 1, 0.5, 1).

Loading the 4 colors into a Gradient structure will look like this:

    Gradient g = new Gradient();

    g.colorKeys = new GradientColorKey[4] {
        new GradientColorKey(cPhase1[0], 0.0f),
        new GradientColorKey(cPhase1[1], 0.33f),
        new GradientColorKey(cPhase1[2], 0.66f),
        new GradientColorKey(cPhase1[3], 1.0f)
    };

Now, we have generated gradients from the base 4 colors, which will allow us to sample intermediate colors:

From 4 base colors to Gradients.

Phase 2 - Tune the final Gradient

With the previous Gradient from 4 base colors, we only need to do 3 final steps to tune the final gradient:

We are going to do all the 3 steps in the same loop of code, but first a bit of explanation.

(1) When we sample the 8 colors, we also want to introduce a bit of randomness on the position of the sample. To achieve this, we are going to split the 4 base color gradient into 8 segments and then we will add a bit of offset before sampling, so the sample is not regular. The offset can't be greater than the size of half of each of the 8 segments. In this case, that offset is going to be Max Offset = (1.0 / 7.0) * 0.5.

Here we can see a visual example:

Pseudo-Random 8 samples of 4 base color gradient.

(2) We then multiply the colors by a factor that, in my particular case, is computed as the Square Root of the sampled position. This makes the initial colors very dim (near black) but quickly progress towards their original colors. This is, of course, completely discretional and it can be changed if another operation fits better our needs.

(3) After multiplying the color, I found very pleasant that the colors located towards the right of the gradient have more "energy" than the initial ones. To achieve this effect, we are going to saturate them. The operation that I use here is to linearly interpolate each color with the color White based on the sample position value to the power of 3. This makes all the colors located at the end of the gradient to quickly converge to color White.

Here we can see the code for these 3 operations:

Color[] cPhase2 = new Color[8];

for (int i = 0; i < 8; i++) {
    float factor = (float)i / 7.0f;
    float eval = factor;
    float offset = (1.0f / 7.0f) * 0.5f;

    eval = Mathf.Clamp01(eval + Random.Range(-offset, offset));

    // (1)
    cPhase2[i] = g.Evaluate(eval);

    // (2)
    float mul = Mathf.Sqrt(factor);
    cPhase2[i] *= Mathf.Lerp(0.1f, 1.0f, mul);

    // (3)
    float sat = factor * factor * factor;
    cPhase2[i] = Color.Lerp(cPhase2[i], Color.white, sat);
}

Finally, we only need to create a new Gradient structure with these 8 new colors:

Gradient g2 = new Gradient();

g2.colorKeys = new GradientColorKey[8] {
    new GradientColorKey(cPhase2[0], 0.0f),
    new GradientColorKey(cPhase2[1], 0.142f),
    new GradientColorKey(cPhase2[2], 0.285f),
    new GradientColorKey(cPhase2[3], 0.428f),
    new GradientColorKey(cPhase2[4], 0.571f),
    new GradientColorKey(cPhase2[5], 0.714f),
    new GradientColorKey(cPhase2[6], 0.857f),
    new GradientColorKey(cPhase2[7], 1.0f),
};

return g2;

And here we can see the final results of this algorithm that transforms 4 base colors into a gradient:

4 base color to gradient.

Also, as a final example, here we can see a gradient generated with this algorithm applied to a skybox generator.

Finished Skybox

   Share Post