Introduction: Exploring Generative Art | How to Create Stunning Artwork Using Randomness

About: I enjoy DIY projects, especially those involving woodworking. I'm an avid computer programmer, computer animator, and electronics enthusiast.

In this Instructable, I will teach you how to create stunning digital artwork using randomness and mathematics. I have written this Instructable assuming the reader has minimal programming background such that this Instructable is accessible to most readers, so if you already understand many of the concepts and background I present, please feel free to skip to the examples section!

The idea of creating artwork using randomness traces back as far as art itself. It wasn't until the advent of computers that artwork from randomness started to reach its true potential - from John Conway's Game of Life to the Electric Sheep project, computers have contributed greatly to the growth of randomized art. The exploration of digital artwork from randomness - hereafter known as Generative Art - is a niche field that's growing more and more popular with the growth of NFTs, or Non-Fungible Tokens. If you don't know what NFTs are, this article from The Verge provides a decent explanation.

But what is generative art, really? There are many definitions, but I prefer to describe it as follows. Generative art is a style of artwork created through a process that integrates random or semi-random behavior within a defined set of mathematical rules. Since randomness and math are the key underpinnings to generative artwork, the only limit is the imagination of the artist. Well, the first limitation really is knowing where to start... which, speaking from personal experience, can be a bit challenging. Fortunately for you, I've got you covered.

I've broken this Instructable into several parts:

  1. Some Fundamentals Of Digital Art
  2. Various Types Of Generative Art
  3. The Leaders In Generative Art
  4. Generative Art Tools
  5. Random Isn't All Random
  6. Example I: Flow Fields
  7. Example II: Perlin Rounding
  8. Example III: Node Motion
  9. Conclusion

My goal is that by the end of this Instructable you will be equipped with the knowledge, tools, and examples that you can utilize and adapt to create your own beautiful works, too!

Step 1: Some Fundamentals of Digital Art

Digital art is one of the newest art mediums that can behave much like other artforms. Some tools allow users to digitally paint, for example. However, digital art carries advantages over other forms of art. One glaring example is the ease with which you can correct mistakes and the simplicity of making variations; another example is the speed to create art can be phenomenally faster.

While the advantages to creating artwork digitally are numerous, it certainly has a number of disadvantages. First, it requires a non-insignificant baseline knowledge of computers and available tools. Second, by virtue of depending on computers, the financial overhead of creating artwork may be higher (computers can be expensive). Lastly, computers have restrictions you definitely don't worry about when creating traditional art, such as a finite number of pixels. My goal with this section is to educate you on a few of these restrictions are and how you can work with them.

Additive vs. Subtractive Color

The first restriction computers are faced with when compared with traditional art methods is a different set of "primary colors." Traditional art methods, such as painting, rely on the subtractive color system, whereas computers instead rely on the additive color system. This difference is a result of light absorption (subtractive) versus light generation (additive); generated light consists of three primary colors: red, green, and blue. As a result, if you're accustomed to using another color system, you'll have to continue to remind yourself which color system computers use.

Pixels Are Blocky!

Computer screens consist of a two-dimensional array of light emitting blocks, called pixels. The pixels of your computer screen are such tiny squares that humans typically cannot distinguish them individually. Similarly, when you use create generative art, your art will be composed of individual pixels. In order to avoid a blocky appearance, you will need your art's canvas to be large - on the order of thousands of pixels on each canvas edge. That being said, you should really scale the dimensions of your canvas based on your intended application. For example, if you choose to print your image on 18" wide x 12" tall paper at 300 dpi (dots per inch) resolution, then you will need your artwork to be 5400 x 3600 pixels for maximum viewing quality.

Pixel Color

Each individual pixel on a computer screen is composed of Red, Green, and Blue (RGB) due to additive color. In terms of computer memory, this means that each pixel is represented by some level of intensity for each color, as level of intensity will correspond to the particular color being produced. Intensity in this case is represented by a number between zero and 255.

But why is the 0-255 range used? Put simply, computer memory is broken down into bytes, where each byte represents eight bits. One bit is either a 0 or 1. Therefore, the number of possible combinations of the eight bits in a byte is 2^8, or 256. If you map each combination of a byte to a decimal number, you get 0-255 (256 total numbers, zero inclusive). All said, a color is therefore represented by three bytes: its red, green, and blue components, which are each in the range of 0-255.

Digital Art's Building Blocks

Drawing with a computer limits our options. We can either compose art pixel-by-pixel, which can be a highly-tedious or mathematically-intensive task, or we can compose art using a set of basic operations, such as drawing lines, shapes, and curves. While it may seem boring/confining to have a limited set of operations, even the most basic operations can lead to stunning results when you sprinkle in some randomization, as you'll see later in this Instructable.

Step 2: Various Types of Generative Art

There are innumerable areas of generative art, so in this section of the Instructable, I will provide a selection of generative art which certainly warrant further investigation by you, the reader, should you be so interested.

Fractals (link)

Fractals are the quintessential form of generative art. In short, fractals are images composed of self-similar patterns containing infinite complexity. If you were to zoom in on a fractal pattern, you would see that same pattern repeat regardless of how far in you zoom. This is a direct result of applying mathematical functions iteratively. In the case of the fractal image below, the fractal is called the Julia Set, created via iterative application of the formula below, where z and c are both imaginary numbers. Of course, there is much more nuance in generating this image than I will go into below!

This image in particular is calculated with the maximum iteration depth of 128 and the escape condition of 512. The bottom-left corner of the image is z = -1.5 - 1.5j, and the top-right corner of the image is z = 1.5 + 1.5j. Interpolate every pixel in between based on your image width and height.

Iterated Function System (IFS) (link)

An IFS is a subset of fractals. Like fractals, they are created via the iterative application of mathematical formulas. However unlike fractals, IFSs involve selecting one of a given set of matrices at each iteration that will be multiplied by the pixel coordinates. Matrices are each assigned a probability of being selected. It's quite spectacular to consider that something so simple can yield such complex results, but it is important to note that the matrices are designed, not simply chosen at random. This method of iterative computation is known as the "Chaos Game," which ends up generating not-so-chaotic images, despite its name.

One prolific example of an IFS is Barnsley's Fern, as seen in the image below. Barnsley's Fern is created using the equations below. I want to emphasize that the pattern of the fern is created by the four matrices specified: F1, F2, F3, and F4. Different patterns may be created by substituting different values in these matrices, adjusting the probabilities, or by increasing/decreasing the number of matrices used. Keep in mind that the sum of all probability terms must equal one.

Note that in order to plot the resulting image, you must convert (x, y) to your image coordinate system. In other words, store all (x, y) values in arrays, locate the minimum and maximum x and y values, and convert these to pixel number via interpolation (as seen in the below formula). You may need to subtract Y from the image height if your (0, 0) position is the bottom left corner as opposed to the top left corner.

Fractal Flames (link)

Fractal flames are one step above IFSs. Instead of using matrices populated with carefully-selected numbers, fractal flames use functions! Each function has an associated probability, as was the case with IFSs. Some examples of functions used are seen below; however, far more functions make up standard fractal flames. I strongly recommend reading the link at the beginning of this section. The PDF is written by Scott Draves, who was the originator of the fractal flame algorithm. When implemented correctly, fractal flames are incredibly brilliant pieces of art. Unfortunately, I've not yet successfully created my own implementation of the fractal flame algorithm, so I do not have any images to present to you here.

Strange Attractors (link)

Strange attractors are a subset of the scientific theory called attractors. Attractors describe a set of dynamic equations in which the system tends to evolve towards a state. The thing that makes attractors so unique, however, is that the state of the system is highly sensitive to the initial conditions. Attractors with dynamic equations that are considered chaotic (chaos theory) are generally called strange attractors, and their overall system is considered stable (the system state does not diverge or grow uncontrollably). This paragraph is dense with new terminology and complicated concepts, but the bottom line is that strange attractors are sensitive to initial conditions and are stable.

The graphic I've created below is the Lorenz Attractor, which is considered the hallmark of strange attractors. The Lorenz Attractor's formulas (a subset of the Navier-Stokes equations) include a third dimension, so in order to plot it in a two-dimensional plane, I only plotted the x and y coordinates. Much like in the cases above, the image is plotted by finding the maximum x and y coordinates and interpolating the pixel locations based on the current (x, y) coordinate.

Harmonographs (link)

As you can see in the harmonograph link I've provided, a harmonograph is a physical pendulum-based device that traces out patterns via the moving pendulum. Often, the harmonograph's drawing surface will move too, providing another axis of motion to create compelling images. Because these physical processes are easily understood, we can write the mathematical processes with the following formulas.

Note that the a terms are amplitude values, f terms are frequency, Φ terms are phase, and d terms are damping. The two formulas can create some compelling images, such as the one below. Note that the added color in this image is the result of gradual color changes as the simulation time increases.

Step 3: The Leaders in Generative Art

If you're hooked as much as I was when I started learning about generative art, the following are people I would consider leaders in the world of generative art. They each have different art forms and styles, but the common thread they share is an innate desire to spread knowledge. Each of these artists have blogs detailing some new and exciting way they've found to create art. Use the tools and knowledge they share to create some awesome art of your own!

  • Jared Tarbell (link)
  • Tyler Hobbs (link)
  • Inigo Quilez (link)
  • Anders Hoff (link)
  • Patricio Gonzalez Vivo & Jen Lowe's Book of Shaders (link)

There are many other artists with insightful blogs and incredible galleries; I encourage you to seek them out!

Step 4: Generative Art Tools

Now let's start to dig into the nitty gritty details about how to create generative art yourself. Generative art can be made with the vast majority of programming languages. In other words, if you already know how to program, you should already have the ability to create generative art.

The most popular tool used to create generative art is Processing. Processing has support for JavaScript, Python, and Android, which means it supports two of the top three most popular programming languages of 2021 (Python & JavaScript). If generative art is your first introduction to programming, I really recommend starting with Processing due to the plethora of documentation surrounding its usage. Furthermore, I'd recommend using the Python version of Processing since the popularity of the Python programming language is growing so fast.

If you are interested in more GPU/shader-focused generative art, I recommend checking out Shader Toy. I've only barely dipped my toes in those waters, and I already can tell it is a powerful tool to create stunning visuals. This tool certainly has a steep learning curve!

A program that has some scripting functionality with a fantastic array of tools is Adobe Illustrator. Many artists use Illustrator to create professional graphics, but it isn't as great as making generative art explicitly.

The last method I'll discuss is the method that I personally use. I use Python to perform CPU-based drawing via Qt. If you haven't already guessed it, I'm a big supporter of Python. When I started learning generative art, I didn't know about the existence of Processing. As a result, I created a drawing library built on the backbone of PyQt's QPainter object. PyQt is included in most standard Python distributions, such as Anaconda, which I linked in the supplies section of this Instructable.

To follow along with the rest of this Instructable you will need a Python installation.

Step 5: Random Isn't All Random

As you may have learned by now, generative art relies on some sort of pattern to create beauty. One way to create patterns is via mathematical formulas. However, sometimes we want to introduce randomness across our canvas instead of using formulas, which may have more predictable results.

If we consider pure randomness, you get an image such as the one below, where every pixel's red, green, and blue components are the same choice of a random number between zero and 255. Unfortunately, this level of randomness isn't overly useful, as the pixel-to-pixel transitions are often abrupt. This abruptness doesn't generally lead to beautiful artwork.

But generative artists are not the first people to encounter this issue. In fact, Ken Perlin, frustrated by the unnatural look of pure randomness created Perlin Noise. At its core, Perlin Noise still uses randomness, but zoomed in and smoothed. To create Perlin Noise, begin by constructing a grid over your image canvas with nx number of columns and ny number of rows. From each corner of each grid square, add unit vectors at random angles (unit vectors have a magnitude of 1). These vectors are called gradient vectors.

For each pixel in the canvas determine which grid square that pixel falls into and construct "offset vectors" from each corner of the grid square, pointing at the pixel. Next, compute the dot product for each pair the grid square's vectors. The result is four values, one for each corner (D1, D2, D3, D4). With each of the four values in hand, compute the two dimensional linear interpolation based on the pixel position relative to each corner, and voila, Perlin Noise!

Ken Perlin later enhanced his creation and coined it Improved Perlin Noise. Improved Perlin Noise doesn't perform pure two-dimensional linear interpolation, but instead computes the interpolation factor via a fade function. The input to the fade function, t, is the interpolation factor term, represented by (dx/x) and (dy/y) in the above graphic. The below graphic shows the fade function and a graph of the fade function. The graph shows how an interpolation factor term maps to the fade function factor. Note that the difference the fade function introduces is a smooth transition between your endpoints, which can yield more natural results. The complete formula for Perlin Noise replaces the (dx/x) and (dy/y) terms with fade(dx/x) and fade(dy/y).

Perlin noise is the foundation of many generative art pieces, and there are ways you can further manipulate it yet! One example I encourage you to research is Cloud Noise (fractal Perlin Noise). Now that you have all of the foundations, you can generate Perlin Noise! Below is an example of Perlin Noise with nx = 5 and ny = 5. If you use Processing, Perlin Noise is already implemented via the noise() function, but if you choose to program in other languages, you may need to write your own Perlin Noise implementation. I have included a Python implementation of Perlin Noise below the graphic to help! This code is also found on my Github.

import math
import numpy as np

def Perlin2D(width, height, n_x, n_y, clampHorizontal=False, clampVertical=False):
    """
    Constructor

    Optimizations were gained from studying:
    https://github.com/pvigier/perlin-numpy/blob/master/perlin_numpy/perlin2d.py

    Parameters:
    -----------
    width : int
        The width of the canvas
    height : int
        The height of the canvas
    n_x : int
        The number of x tiles; must correspond to an integer x-edge length
    n_y : int
        The number of y tiles; must correspond to an integer y-edge length
    clampHorizontal : boolean
        Imagine the Perlin Noise on a sheet of paper - form a cylinder with
        the horizontal edges. If True, cylinder will be continuous noise
    clampVertical : boolean
        Imagine the Perlin Noise on a sheet of paper - form a cylinder with
        the vertical edges. If True, cylinder will be continuous noise

    Returns:
    --------
    <value> : numpy array
        noise values for array[width, height] between -1 and 1
    """
    # First ensure even number of n_x and n_y divide into the width and height,
    # respectively
    msg = 'n_x and n_y must evenly divide into width and height, respectively'
    assert width % n_x == 0 and height % n_y == 0, msg

    # We start off by defining our interpolation function
    def fade(t):
        return t * t * t * (t * (t * 6 - 15) + 10)

    # Next, we generate the gradients that we are using for each corner point
    # of the grid
    angles = 2 * np.pi * np.random.rand(n_x + 1, n_y + 1)
    r = math.sqrt(2)  # The radius of the unit circle
    gradients = np.dstack((r * np.cos(angles), r * np.sin(angles)))

    # Now, if the user has chosen to clamp at all, set the first and last row/
    # column equal to one another
    if clampHorizontal:
        gradients[-1, :] = gradients[0, :]
    if clampVertical:
        gradients[:, -1] = gradients[:, 0]

    # Now that gradient vectors are complete, we need to create the normalized
    # distance from each point to its starting grid point. In other words, this
    # is the normalized distance from the grid tile's origin based upon the
    # grid tile's width and height
    delta = (n_x / width, n_y / height)
    grid = np.mgrid[0:n_x:delta[0], 0:n_y:delta[1]].transpose(1, 2, 0) % 1

    # At this point, we need to compute the dot products for each corner of the
    # grid. To do this, we first need proper-dimensioned gradient vectors - do
    # this now. A computation for number of points per tile is needed as well
    px, py = int(width / n_x), int(height / n_y)
    gradients = gradients.repeat(px, 0).repeat(py, 1)
    g00 = gradients[:-px, :-py]
    g10 = gradients[px:, :-py]
    g01 = gradients[:-px, py:]
    g11 = gradients[px:, py:]

    # Compute dot products for each corner
    d00 = np.sum(g00 * grid, 2)
    d10 = np.sum(g10 * np.dstack((grid[:, :, 0] - 1, grid[:, :, 1])), 2)
    d01 = np.sum(g01 * np.dstack((grid[:, :, 0], grid[:, :, 1] - 1)), 2)
    d11 = np.sum(g11 * np.dstack((grid[:, :, 0] - 1, grid[:, :, 1] - 1)), 2)

    # We're doing improved perlin noise, so we use a fade function to compute
    # the x and y fractions used in the linear interpolation computation
    # t is the faded grid
    # u is the faded dot product between the top corners
    # v is the faded dot product between the bottom corners
    t = fade(grid)
    u = d00 + t[:, :, 0] * (d10 - d00)
    v = d01 + t[:, :, 0] * (d11 - d01)

    # Now perform the second dimension's linear interpolation to return value
    return u + t[:, :, 1] * (v - u)

Step 6: Example I: Flow Fields

In my first example, I will demonstrate how to use Perlin Noise to create what's called a flow field. The simplest way I can explain the concept is as follows. Imagine the Perlin Noise image above as a terrain map, where lighter colors represent peaks and darker colors represent valleys. Now imagine placing a marble anywhere on the image and watching it travel across the terrain, eventually setting in a valley. A flow field is basically a way to represent how the marble will travel, only with flow fields, the marble's momentum is not taken into account. As such, the underlying mechanism of a flow field is a vector field, and the flow field is basically playing connect-the-dot. Let's decompose this visually:

(1) Start with a Perlin Noise image

(2) Overlay the vector field

(3) Connect the dots

That's how to make a basic flow field! The code to create these three images is found on my GitHub here.

From here, the possibilities are really endless, you can add color based on certain conditions, adjust the line lengths, make curves thicker or thinner, modify where the curves begin, customize the image dimensions, etc. Below are a few examples of some outputs from this flow field function I wrote. I encourage you to copy this code and make it your own! Learning from examples is a great way to start creating art from code.

Step 7: Example II: Perlin Rounding

This second example is another flow field example, except for one glaring change. Instead of a smooth underlying vector field, I round each one of the vectors to the nearest 45° with the below line of code. The complete sample code for this section can be found here. When using just a black and white color palette, the results from this one change are fantastic!

angle = math.pi*(2*noise[int(x_s), int(y_s)] - 1)

Step 8: Example III: Node Motion

In this third and final example, we will take a little different approach. Consider a system of points arranged in a circle. Each point connects to the adjacent point, thereby tracing out a circle. Consider now that each of these points, already having a defined position, is now assigned a unique velocity. Now, step through time, adjusting the position (and possibly velocity) of each point until a certain amount of time has passed. For each time step, draw the "circle" body's shape on the same canvas such that you can see how the shape transforms with time. What would be the result?

Well, that would depend on how we assign the velocity to each point. If you first try random velocities between -2 and 2 pixels/second for a canvas width and height of 2000x2000 pixels, you get patterns such as those shown in the below panel.

Note that these patterns are... well, chaotic to say the least. Probably not what most people would call appealing. Clearly the sharp transitions between velocity values is an issue. The good news is we've solved this problem before with Perlin Noise. Instead of pure randomness, if we multiply a maximum velocity by the value of an underlying Perlin Noise field, we can add order to the disorder! Note that this means the velocity changes at every update, but that's not a problem since this is art after all. Certainly you can experiment with setting a static velocity based on an initial sampling of the Perlin Noise field - I encourage you to try this! The result of this improved velocity selection can be seen in the below panel.

Definitely better! The code on my GitHub was used to generate the panel images above. If you set the random seed the the values noted in the panels, you should get the same results. There are a number of "knobs" you can turn to see how they affect the resulting image. At a minimum, these knobs include: adding color, changing the transparency of the pen, creating your own unique velocity-selection method, changing the radius of the circle, using a shape other than a circle, adjusting the position of the circle, adjusting the velocity settings, and adjusting the number of x and y tiles in the Perlin Noise field. Clearly the sky is the limit!

Step 9: Conclusion

By now, I'm hoping to have whetted your appetited for generative art. This is truly an exciting field for hobbyists and professionals alike. With computers so powerful and tutorials so numerous these days, the opportunities to start learning and creating your own art are endless. If you have any questions or comments, I would love to hear them! As well, if you find any bugs, please let me know!

Cheers!

Made with Math Contest

Second Prize in the
Made with Math Contest