Mining Structures walkthrough

Laxraven and I just released our collaborative project “Mining Structures” on FxHash. It was a huge success, and I have received many questions about the process and techniques used. Here is a quick walkthrough.

The project started with a failure. @Laxraven had posted a beautiful architectural drawing on Twitter, and someone mentioned they would love to see a generative version – to which I foolishly replied, “Challenge accepted”.
As it turned out, this initial drawing was pretty difficult to convert to generative. Peter had used very aggressive, loose lines in his sketch, and my initial attempts at emulating these were, well, not that great. So after some discussions we decided to recreate a different piece that would be more suitable. I chose his “Structure holding object” drawing:

“Structure holding object” by @laxraven

Analog lines

Looking at the details of the original drawing, it was quickly obvious that I needed a way to draw lines with a natural feel to them. Peter’s line are never perfectly straight, some of them have little splatters of ink at the end of the line, or little ink bubbles… So the first step was to write a “natural line engine” in P5.js. I would share the code, but I’m a big adept of “teach a man/woman to fish” principle, so I would rather deconstruct the logic and see what others come up with. If you really want the code, it is on the blockchain.

Here are the features I ended up with. In the examples below I am drawing a straight horizontal line.

“analogueness” – this splits the line in a lot of different points (the longer the line, the more points) and displaces each of them randomly. A javascript function is used to control the line weight across the progress of the line.

“Analogueness” feature
// color function = black the whole way
var cfunc = (idx, p) => color(0); 
// weight function = randomized, but increasing
var wfunc = (idx, p) => ((1+8*idx+3*rnd())/W); 
var lineDrawer = new LineDrawer(cfunc, wfunc);
// how often to split, and how much to displace
lineDrawer.setAnalogueness(0.1, 0.01);
// draw horizontal line
lineDrawer.draw(vec2(-0.7, 1.05), vec2(0.7, 1.05));

end bubble probability” – this controls the probability that the line ends with a little ink ‘bubble.

“end bubble” feature
// draw line straight
lineDrawer.setAnalogueness(0.1, 0.0);
// with a bubble at the end

“splatter probability” – this controls the probability that the line has little ink splatters at the end. They typically increase in distance and shrink in size. I have parameters to control the minimum line length before splatter occurs, the maximum splatter distance, the splatter probability and the maximum number of repetitions.

// draw line straight
lineDrawer.setAnalogueness(0.1, 0.0);
// with splatter at the end
lineDrawer.setSplatter(10/W, 40/W, 1.0, 5, 1.25);

When combining all the feature together, we get some lovely organic flow with very little code:

lineDrawer.setAnalogueness(0.1, 0.01);
lineDrawer.setSplatter(10/W, 40/W, 0.1, 5, 1.125);
for (var i=0; i<10; ++i)
lineDrawer.draw(drawingLayer, vec2(-0.7, 0.5-0.02*i), vec2(0.7, -0.02*i));


“How did you do the shading on the rock ?” is probably the most frequently asked question. Again I’ll deconstruct it, using a circle as an example.

1. draw lines along the circle in the SE direction

2. adjust the line lengths based on their position on the circle

3. Randomize line lengths a bit

4. Do it again in the NE direction.

5. And again in the NW direction

5. Clip drawing to the original circle

6. Turn on “analog line drawing” features

Drawing the scaffolding

With the line drawing engine in place, this part was rather straightforward. I drew it in 3 layers with various levels of transparency to give it a sense of depth. Each layer is made of vertical rectangles of random heights that are randomly split and displaced.

First layer
First+second layers
All 3 layers

Drawing the ropes

These were not in the original drawing, but we decided they would add a nice textural change to the piece.
They are made by picking random pairs of points within the scaffolding (or the main rock) and drawing a parabola between them. The parabola is adjusted so the middle point has the same height than the lowest point in the pair. This made the math somewhat easy to deal with.

Adding analog elements

The generative drawing was starting to look pretty good at this point, but it took another dimension entirely once we added analog elements on the ground line. Laxraven drew several trees, houses, and rocks using ink on paper, scanned them, and we mixed them with the generated drawing, taking special care to match the line weights so they would blend seamlessly. This really perfected the illusion, making it very difficult for the viewer to distinguish the analog elements from the generated ones.

During this last phase we also spent a lot of time adding rarity features, such as a random crack in the rock, sometimes filled with gold or opal colors, as well as the ultra-rare “frog” rock.

This project was a lot of fun and I’m looking forward to the next collab with Laxraven !

Here are a few images from the series (including some outtakes from the development that didn’t make it into the final collection).
You can also view the full collection in its high resolution glory (and generate an infinity of variations) at fxhash.


Image 1 of 11


Ammonites walkthrough

Following the unexpected success of the Ammonites NFT on fxhash, several people have asked me to give more information about my approach for producing this type of generative pieces.

So here it is, I’m going to take this image and deconstruct it for you, step by step.

1. The easy part first – the background paper

The background canvas is generated with noise and random vertical / horizontal lines. With exaggerated contrast and in greyscale, it looks like this:

The code for generating these is fairly simple:

function noise_pattern(layer, fg,bg, N, ll)
	layer.strokeWeight(1.0 / layer.width);
	layer.rect(-1, -1, 2, 2);

	ll = ll / layer.width * (1.0+rnd());
	for (let i = 0; i < N; i++)
		let x1 = rrnd(-1 + ll, 1 - ll);
		let y1 = rrnd(-1, 1);
		let x2 = rrnd(-1, 1);
		let y2 = rrnd(-1 + ll, 1 - ll);
		layer.circle(x1, y1,ll);

function canvas_pattern(layer, fg, bg, N, ll)
	layer.strokeWeight(1.0 / layer.width);
	layer.rect(-1, -1, 2, 2);

	ll = ll / layer.width * (1.0+rnd());
	for (let i = 0; i < N; i++)
		let x1 = rrnd(-1 + ll, 1 - ll);
		let y1 = rrnd(-1, 1);
		let x2 = rrnd(-1, 1);
		let y2 = rrnd(-1 + ll, 1 - ll);
		layer.line(x1 - ll, y2, x1 + ll, y2);
		layer.line(x2, y1 - ll, x2, y1 + ll);

canvas_pattern(backgroundLayer, color('#00000008'), color('#b08a65'), 1000, 100);
noise_pattern(backgroundLayer, color('#00000004'), color('#00000004'), 10000, 10);
noise_pattern(backgroundLayer, color('#00000004'), color('#00000004'), 20000, 4);

Note that I normalize my canvas to -1..1 units, instead of using pixel units.

If you look closely at the frame lines, you will notice they are not perfectly straight, but are a little shaky, to imitate the feel of hand drawn lines. This is the kind of tiny detail that came in late in the development, but I feel is extremely important to add that final polish. The shaky line is drawn by subdividing the whole line in 100 segments, and applying a tiny bit of randomization to each segment point.

2. Drawing spirals

The general formula for a spiral shape is well-known, and looks like (in polar coordinates)

r = e^(b θ) – different values of b give different spiral growths.

Next, we can scale the spiral to define the inner and outer shell locations. If they overlap, it’s a classic nautilus structure. If they don’t, we get an unrolled shell.

3. Shell subdivisions

Ammonites are made of consecutive chambers. Another parameter I introduced is the “number of chambers per rotation”, i.e, how many chambers are in a 2π turn around the spiral. Here is an example with RotationSteps = 7.

This number doesn’t have to be a integer, as a matter of fact non-integer values produce more complex patterns that are visually interesting. Here is one with RotationSteps = 8.5

and our image at the top of this page, stripped down to the bare essentials, looks like this (RotationSteps = 13.58)

4. Outer spiral decoration

Now that our shell is defined with an outer and inner edge, we can add decorations (spikes, knobs, ridges, …) to the outer edge.
For each chamber we have an index that goes from 0 to 1, and a ‘decoration function’ that produces the various decoration types.

For example here is a function that produces a spike in the middle of the chamber:

function addMiddleSpike(x, w, amount)
	if (x < 0) x = 0;
	else if (x > 1) x = 1;
	if (Math.abs(x - 0.5) < w)
		var t = (1.0 - Math.abs(x - 0.5) / w);
		return t * t * t * amount;
	return 0;

The various functions for producing the decorations are fairly simple, have quite a few parameters, and when combined together, provide a huge variety of designs with a lot of room for randomization – ideal for generative designs !

Here is our initial image, with its outer decoration applied:

5. Line weight control

This step is so crucial that I feel it deserved a paragraph on its own. Traditional artists (working with ink) add a lot of variation to their lines thickness. So far our drawing looks very flat, so let’s make the outer spiral lines thicker at the bottom, and thinner at the top, and do the opposite for the inner spiral lines:

It’s a subtle difference, but all these little details compound in the end !

6. Drop shadows

This one is rather straightforward, I used the HTML canvas drop shadow features. P5.js doesn’t expose these, so we need to work with the device context directly. I vary the alpha of the shadow based on the distance from the center, and only draw the shadows for the last turn of the spiral.

const stepThreshold = RotationSteps * 2 | 0;
drawingLayer.drawingContext.shadowBlur = 24;
drawingLayer.drawingContext.shadowOffsetX = 3; 
drawingLayer.drawingContext.shadowOffsetY = 6; 
var shadowAlpha = (step > stepThreshold) ? 10 * (step - stepThreshold) : 0;
if (shadowAlpha > 200) shadowAlpha = 200;
drawingLayer.drawingContext.shadowColor = color(0, 0, 0, shadowAlpha);

7. Spiral shadow

Let’s add some sense of volume to our otherwise flat spiral, by drawing some more shadows around the edges. I basically walk around the spiral, drawing almost transparent circles with a brush that increases in size.

function drawShadows(nsteps)

	var brushSize1 = function (x)
		return 0.001 + 0.05 * x * x * x * x * x;
	var brushSize2 = function (x)
		return 0.001 + 0.2 * x * x * x * x * x;
	draw_function_with_circle_brush(drawingLayer, function (x)
		return outerSpiral(stepToAngle((nsteps - 0.1) * x)).smul(0.99);
		brushSize1, 500, 100, 500);
	draw_function_with_circle_brush(drawingLayer, function (x)
		return outerSpiral(stepToAngle((nsteps - 0.1) * x)).smul(0.92);
		brushSize2, 500, 100, 499);

	draw_function_with_circle_brush(drawingLayer, function (x)
		return innerSpiral(stepToAngle((nsteps - 0.1) * x)).smul(1.01);
		brushSize2, 200, 50, 199);


and here is the end result:

8. Suture patterns

Ammonites have decorative suture patterns, which are textural lines separating the chambers. There are many different types of suture patterns, and I wish I had spent more time on these, as some of the patterns on the actual fossils are exquisitely intricate. I used simple parametric equations to draw 2d curves along the radial axis. Having several equations allowed me to add more entropy to the collection.

Here is our picture, with its suture patterns drawn.

9. Contour hatching

That was the hardest part to get right. I added hatching in 2 directions, first around the outer edge:

and then radially, using a variable alpha so the radial lines are transparent in the center:

Finally I also added a second radial pass, using white highlights, with alpha fading to transparent towards the edges:

10. Radial lines accents

I also added a feature to accent every N chamber separation. In our example N = 3, and it looks like this with the accents added:

11. Closing the shell

Here I close the shell by drawing a symmetrical version of the final radial line, and filling the gap with a dark color. This part was incredibly hard to get right in all cases, as the geometry around the closure can be pretty complicated with spikes and ridges… I ended up drawing the full shape on a different layer, and then using P5.js erase() mode to remove the part from the previous turn that should still be visible.

11. Magic blend trick for extra contrast

Finally, to add more contrast to the final image, I used P5.js blend() in SOFT_LIGHT mode, blending the image onto itself. This gave it this extra pop you can see in the picture at the top of the page.

12. Rare “ammolite” version

Some ammonites are made of an opal-like mineral that’s absolutely beautiful. I tried to recreate the effect in a very small (~4 %) number of editions. I picked colors from actual ammolite stone, and blended them over the shell contour with variable transparency and brush sizes.

I think it’s nice to add rare features like this, as people who mint one of these get really excited and generate a lot of buzz.

13. Closing notes

In conclusion, for this type of illustrations I believe that the devil is really in the details. All these little things that you can barely see individually, all add up to elevate the piece.

I had a lot of fun designing this, even though towards the end, I was seeing spirals in my sleep !

For any questions, hit me up on Twitter or FxHash discord.