Solace
Solace is a generative algorithm that draws hundreds of thousands of tiny points to produce artworks of sand dunes. It was released as a series of 240 editions on fxhash in February 2022.
This write-up contains two sections. The first section explores the range of artworks that can be generated by the algorithm while the second section is a more technical overview of how the algorithm works.
1. Exploring the outputs
The code begins by randomly selecting values for around thirty variables which determine how the generated artwork will look. Ten of these variables are features that are publicly visible, such as number of dunes, colour scheme, brush type or sky type. Others variables only exist in the code such as how dark the sky is or how steep the dunes are.
This section explores these different variables and how they affect the algorithm’s output.
Number of dunes
Some outputs consist of a single sand dune while others feature a landscape covered by dunes. The number of dunes usually ranges from 1 up to 48, but there’s also a few outputs with 120 dunes.
The same logic is used to draw each dune regardless of how many there are. Some outputs like the one on the left also have a gust of wind lifting up sand at the peak.
Sky type
There are seven ways the sky can be drawn: Beam, Timelapse, Null, Whisper, Starry, Wool and Tabula.
Beam and Timelapse produce dynamic and energetic skies.
Null and Whisper produce more tranquil scenes.
Starry and Wool were inspired by stars and clouds. The output on the right also has the Dusty feature which adds scratches and sparks to the sky.
Tabula is perhaps the most distinctive sky type.
Brush type
Most outputs use the Sand brush type, but there are also two other brush types: Soft and Grainy. The brush type determines the way points are drawn onto the canvas.
The Soft brush draws points with more transparency, which makes them lighter and softer.
The Grainy brush draws fewer points, but each point is larger. Grainy was actually the only brush type used early on in the project. It was only later when I experimented with other ways of drawing points that I found softer and finer brush types also worked well.
When working on the code, I would sometimes override it to use the Grainy brush, as it renders the output more quickly, which helped speed up the feedback cycle.
Mist
Mist is a feature that determines whether or not a soft layer of mist is drawn between dunes. It was inspired by tufts of sand lifted up by the wind, catching the sunlight.
Since I liked mist and no mist equally, I chose to make them appear with roughly equal probability rather than making one of them rare. Initially I was wary of splitting a feature 50-50 as it could risk making the collection feel like two separate ones combined together, but in this case I felt it didn’t make it any less cohesive.
Dancers and Sandstorm
Dancers and Sandstorm are rare features which only appear in a small number of the 240 minted outputs. Dancers make the dunes sway from side to side while Sandstorm adds messy lines that leap out from between the dunes.
Both of these features were actually simple tweaks made to the logic, only requiring a few lines of code each to implement. Dancers adds a sideways warp; Sandstorm adds a low probability chance of error when labelling regions as dark or light.
I was secretly hoping that one of the 240 minted outputs would be both Dancers and Sandstorm, but unfortunately none showed up.
Render effect
The algorithm can take several seconds to run so it was important to consider the experience for the viewer during this loading time. Rather than seeing a blank canvas then having the whole image appear at once, points are drawn gradually in a certain order to produce an animated effect.
There are four possible render types: Simple, Banners, Refine and Modulo. Simple is the default and draws the image from top to bottom. Modulo is the most intricate - it draws points in a fixed direction, wrapping around when it hits the edge. It uses a mathematical property to ensure all parts of the canvas are covered equally.
Examples of the different render types can be seen in the live views:
Palette
There are eighteen possible colour palettes that were curated by hand. Some outputs from the three most commonly occurring palettes, Warm, Weather and Lamp, are shown below. The more common palettes were intended to look more natural or sand-coloured.
There are also other rarer and more distinctive palettes. The output on the left is one of only three with the Lychee palette and the one on the right is one of five Leaf palettes.
There ended up being only one output with the Underworld palette. Actually, I was relieved it showed up - it would’ve been a shame if there was a palette that was never used.
Other variables
The last of the ten publicly visible features is Margin, which can add a thin or thick border around the edge.
There’s also a bunch of other variables in the code which affect how the output looks. Only the variables which cause the most recognisable differences were labelled as features.
These other variables include the darkness of the sky, how high up the horizon is, the curviness or waviness of the dune spines, the size of the triangle patterns along the spines, whether the dune peaks are more rounded or more pointy, whether the dunes lean to one side, whether there are sand lines and how wide they are and how precisely the points are drawn (some outputs have a slight horizontal blur).
2. Algorithm overview
This section summarises how the algorithm works and includes some of the more interesting technical details. If you have any questions or want to learn more in depth, feel free to message me on Twitter (@lunarean).
Drawing points
In short, the algorithm works by drawing hundreds of thousands of random points on the canvas.
For a given location on the canvas, it calculates how dark it should be. For example if it’s on the shadowy side of a dune, it should be darker. If it’s in the middle of a cloud, it should be lighter.
This is then converted into a rejection probability - the probability that a point isn’t drawn. For example, a point in a dark location might have a rejection probability of 0.1, which means there’s a 90% chance it does end up getting drawn.
Drawing spines
The algorithm starts by placing dunes randomly on the canvas ensuring no two dunes are too close to each other. For each dune, it generates a spine - the wavy curve that separates the two sides of the dune. The shape of the spine is actually two sine waves added together, which is my go-to method for creating wavy curves.
I also found it was more natural for the amplitude of the sine waves to increase the further down it goes, to make the peak look more pointy. This also ended up occasionally producing sweeping ridges at the base of the dunes.
calcShade(x, y)
There’s a crucial function in the code called calcShade(x, y) which, given a coordinate (x, y), calculates whether that point is located on the dark side of a dune, the light side of a dune or in the sky. I ended up using a pretty convoluted method to do this for reasons I’ll explain later.
First, a gradient is randomly selected for the dune. A low gradient means the dune is more flat and a high gradient means it’s steep and pointy. The algorithm walks along the spine and at intervals, it looks at its (x, y) position and stores two values: y + gradient * x and y - gradient * x.
Later on, if it wants to know how to draw another point (x, y), it can just check if y + gradient * x or y - gradient * x were stored. If y - gradient * x is found, it means that point is to the left of the spine; if y + gradient * x is found, it’s to the right. If both are found, it uses the side that was stored later. Note that this only works if the gradient of the spine is steeper than gradient.
Dunes are checked in order from front to back, which usually means from the bottom of the canvas to the top. This ensures dunes aren’t covered up by other dunes behind it. If a point is not found in any dune, it’s labelled as being in the sky.
Another benefit to having a method like calcShade() is it makes it easy to add mist or shadows behind dunes. A point (x, y) could be labelled as mist or shadow if (x, y + dy), a point slightly below it, is part of a different dune - something that can be checked easily in calcShade().
Artifacts
There are actually easier and more reliable methods to shade the two sides of a dune, such as filling in a polygon with vertices along the curve, or drawing lots of thin lines to the left and right from points on the curve. However, the calcShade() method was used because it produces nice grainy textures when combined with the method of drawing lots of random points. In addition, it produced some unintended but fascinating artifacts.
One of these artifacts was the triangle patterns along the dune spine. These weren’t hard-coded but emerged as a result of precision errors near the spine. When the algorithm walks along the length of the spine, it walks in discrete steps, jumping a certain stepSize with each step. It’s the gap between these steps that cause errors in the sampling and produce the patterns along the spine.
Some outputs, such as the one below, used a very small stepSize and therefore produce much sharper ridges without the patterns along the spine.
The thin light stripes on the dark side of the dune were another surprising detail that emerged. These can occur near the base of the dunes where the sweep of the spine is wider.
Another detail that sometimes occurred was a single sand line randomly being thicker, such as in the output below. This actually originated from the value of the modulo a % b becoming negative when a is negative - there’s a variable that turns from positive to negative where the thick line is located.
Domain warp
The last step is to apply a domain warp to the entire image which stretches different parts of the canvas. These are functions that map each coordinate to a new location such as (x, y) → (x, y²), which replaces a point’s y-coordinate with its square. This would move a point like (0.8, 0.8) to (0.8, 0.64), which has the effect of vertically stretching one side of the image and compressing the other side. Trigonometric functions such as (x, y) → (x, y + sin(ax + b)) are also used to add subtle waves to the dunes.
Without any warping, the dunes would be triangles with straight edges. The output below has a large amount of warp applied, making parts of the dunes bulge outwards and other parts curve inwards.
Thank you for reading!
If you wish to view the entire collection, a great place to do this is on fxfam.xyz. If you’re interested in collecting, there is a secondary market on fxhash.
Follow me on twitter for updates on my future work, and please feel free to reach out if you have any questions!
lunarean