Tyler Hobbs

View Original

Creating Soft Textures Generatively

Most of the generative artwork I see exclusively uses hard, sharp transitions. Some pieces use smooth gradients or basic transparency to create transitions. Very few pieces go beyond that. In my own work, my preference is to create some soft areas with few or no distinct edges. This happens naturally when working with analog media like paints, pastels, or pencils, but in programmatic artwork, you usually have to work for it.

As I see it, there are three main tools that you can use for creating soft areas:

  • Layered transparent objects

  • Small or fine objects repeated many times

  • Creating a blur effect through direct pixel manipulation

I will cover the first two options. These come the most naturally to me, and I have much more experience with them. Direct pixel manipulation can also create a huge variety of interesting effects, so don't hesitate to explore that on your own.

The examples below use Processing through Quil, a Clojure wrapper. You can use my tutorial on Quil to set up an environment if you'd like to run the example code directly. Each of the example images are created in a 300x600 image.

Before each of the code examples below, the following setup code will be run:

; make the color mode HSB with hue in [0, 360], saturation in [0, 100], ; brightness in [0, 100], and alpha in [0.0, 1.0] (color-mode :hsb 360 100 100 1.0)  ; make the background white (background 0 0 100)  ; define top-left corner of rectangle, width, and height (def left-x 20) (def top-y 20) (def rect-height 260) (def minimum-width 360)  ; don't create outlines on polygons (no-stroke) 

LAYERING TRANSPARENT OBJECTS

Layering transparent objects is the most straightforward technique. Typically you will stack slightly-offset polygons with a low-opacity fill. The basic technique looks something like this:

; fill the rectangles with gray with 0.1 alpha (fill 0 0 50 0.1)  ; draw the rectangle layers (doseq [i (range 20)]   (let [actual-width (+ minimum-width (* 10 i))]     (rect left-x top-y actual-width rect-height))) 

This will produce a rectangle that looks something like this:

The quality of the soft transitional region can be modified in a few ways. First, you can change the opacity and number of repetitions to get a smoother (or rougher) blend:

; use 2% opacity (fill 0 0 50 0.02)  (doseq [i (range 100)]   (let [actual-width (+ minimum-width (* 2 i))]     (rect left-x top-y actual-width rect-height))) 

With ten times as many layers, the effect is much smoother:

Second, you can change the length over which the soft area is blended (I'll make it shorter):

(def minimum-width 520) (fill 0 0 50 0.1) (doseq [i (range 20)]   (let [actual-width (+ minimum-width (* 2 i))]     (rect left-x top-y actual-width rect-height))) 

The transition is 1/5th as long now:

So far the soft transition area is quite uniform. One easy way to give it more character is to use a probability distribution for the width:

(doseq [i (range 20)]   (let [actual-width (+ minimum-width (random 0 200))]     (rect left-x top-y actual-width rect-height))) 

By adding a (uniformly) random length between 0 and 200 to the width, we get a rectangle that looks like this:

Using a Gaussian distribution will give us a different pattern. Typically, it will have more segments at the start, but a few extreme outliers.

(defn gauss [mean variance]   (+ mean (* variance (random-gaussian))))  (doseq [i (range 20)]   (let [actual-width (+ minimum-width (abs (gauss 0 100)))]     (rect left-x top-y actual-width rect-height))) 

This produces something with a little more variety:

So far, all of the soft transitional regions have been rectangular in shape and in their transitional layers. To produce different effects, we can modify the shape of the (previously rectangular) polygon on the transitional side. Let's start with a basic example: extending the top and bottom by random amounts:

(doseq [i (range 20)]   (let [top-width (+ minimum-width (abs (gauss 0 100)))         bottom-width (+ minimum-width (abs (gauss 0 100)))]      ; in Processing, the y-axis is inverted with 0 at the top     (begin-shape)     (vertex left-x                  top-y)            ; top left     (vertex (+ left-x top-width)    top-y)            ; top right     (vertex (+ left-x bottom-width) (+ top-y rect-height)) ; bottom right     (vertex left-x                  (+ top-y rect-height)) ; bottom left     (end-shape :close))) 

This is where things start to get interesting. We get some pretty cool effects in the transition region:

We can produce a different shape by adjusting the polygon's right side. This code will produce a fan-out effect:

; leave space at the top and bottom for fan-out (def top-y 30) (def rect-height 240)  (doseq [i (range 20)] (let [top-x-extend (abs (gauss 0 100))       top-width (+ minimum-width top-x-extend)       right-top-offset (* -1 (abs (gauss (* 0.1 top-x-extend) 1.0)))        bottom-x-extend (abs (gauss 0 100))       bottom-width (+ minimum-width bottom-x-extend)       right-bottom-offset (abs (gauss (* 0.1 bottom-x-extend) 1.0))]    (begin-shape)    ; top left   (vertex left-x                   top-y)   ; begin top fan-out   (vertex (+ left-x minimum-width) top-y)   ; top right   (vertex (+ left-x top-width)     (+ top-y right-top-offset))   ; bottom right   (vertex (+ left-x bottom-width)  (+ top-y rect-height right-bottom-offset))   ;begin bottom fan-out   (vertex (+ left-x minimum-width) (+ top-y rect-height))   ; bottom left   (vertex left-x                   (+ top-y rect-height))    (end-shape :close)))

Or, you can avoid sharp corners by using bezier curves for the right side:

(doseq [i (range 20)]   (let [top-width (+ minimum-width (abs (gauss 0 20)))         control-1-width (+ minimum-width 40 (abs (gauss 0 100)))         control-2-width (+ minimum-width 40 (abs (gauss 0 100)))         bottom-width (+ minimum-width (abs (gauss 0 20)))]      (begin-shape)     (vertex left-x top-y) ; top left      ; make the curve     ; top right     (vertex (+ left-x top-width) top-y)      (bezier-vertex       ; control 1       (+ left-x control-1-width) (+ top-y (* 0.333 rect-height))       ; control 2       (+ left-x control-2-width) (+ top-y (* 0.666 rect-height))       ; bottom right       (+ left-x bottom-width)    (+ top-y rect-height))      ; bottom left     (vertex left-x (+ top-y rect-height))     (end-shape :close)))

REPETITION OF SMALL OBJECTS

My preferred way to create soft transitional areas is by laying down a bunch of very small objects. Usually these are dots, lines, or curves, but other polygons work as well. This tends to be more computationally expensive than the previous technique of layering transparent polygons, but the effects can be well worth it.

Let's begin with a very simple technique: spreading dots out in a Gaussian distribution away from the rectangle.

; make the fill fully opaque (fill 0 0 50 1.0)  ; fill in the non-transitional area of the rectangle (rect left-x top-y minimum-width rect-height)  ; draw dots (doseq [i (range 20000)]   (let [x (+ minimum-width (abs (gauss 0 60)))         y (random top-y (+ top-y (- rect-height 2)))]     (rect x y 2 2))) 

The effect is a very smooth, even fade out:

For aesthetic reasons, I usually prefer to work with lines instead of dots. There are many, many interesting things you can do with a lot of lines. I'll cover some of the simplest techniques first. For example, you can extend lines horizontall from the rectangle using a Gaussian distribution for the length of the line:

(fill 0 0 50 1.0)  ; black with 0.1 alpha (rect left-x top-y minimum-width rect-height)  (stroke-weight 0.1) (stroke 0 0 50 1.0) (doseq [i (range 5000)]   (let [line-start-x (+ left-x minimum-width)         line-end-x (+ left-x minimum-width (abs (gauss 0 60)))         y (random top-y (+ top-y rect-height))]     (line line-start-x y line-end-x y))) 

This gives a neat sort of "smear" effect:

If horizontal lines work, why not vertical lines?

(fill 0 0 50 1.0) (rect left-x top-y minimum-width rect-height)  (stroke-weight 0.1) (stroke 0 0 50 1.0) (doseq [i (range 1000)]   (let [x (+ left-x minimum-width (abs (gauss 0 60)))         bottom-y (+ top-y rect-height -1)]     (line x top-y x bottom-y)))

What if we don't use a particular direction for the lines at all?

(fill 0 0 50 1.0) (rect left-x top-y minimum-width rect-height)  (stroke-weight 0.1) (stroke 0 0 50 1.0) (doseq [i (range 8000)]   (let [top-x (+ left-x minimum-width (gauss 0 50))         bottom-x (+ left-x minimum-width (gauss 0 50))         bottom-y (+ top-y rect-height)]     (line top-x (random top-y bottom-y) bottom-x (random top-y bottom-y))) 

The result is much more chaotic and full of energy:

Now we move on to one of my favorite techniques for laying down lots of small objects. Adding slight, loose patterns to the mostly-random small objects can create a lot of interesting effects. Even subtle patterns show up pretty clearly, and you can get a lot of variety in the results. To start, let's use random directional lines as before, but cluster the lines so that several of them point in nearly the same direction:

(fill 0 0 50 1.0) (rect left-x top-y minimum-width rect-height)  (stroke-weight 0.1) (stroke 0 0 50 1.0) (doseq [i (range 1000)]   (let [top-x (+ left-x minimum-width (gauss 0 60))         bottom-x (+ left-x minimum-width (gauss 0 60))         bottom-y (+ top-y rect-height)         mean-start-y (random top-y bottom-y)         mean-finish-y (random top-y bottom-y)]     (doseq [i (range 8)]       (line (gauss top-x 5)             (gauss mean-start-y 5)             (gauss bottom-x 5)             (gauss mean-finish-y 5))))) 

Like before, the result is chaotic and energetic, but now it has a bit of structure and rhythm. To me, it's reminiscent of a bird's nest or a bundle of sticks.

WRAP UP

The examples I gave here are just a simple starting point for your own experimentation. Try some crazy ideas and see what works. If you come up with something cool, email or tweet me and let me know!

Cheers!


See this form in the original post