Cracking

Ages ago, 2006 or so, I came across a small book, Pamphlet Architecture 27: Tooling by Benjamin Aranda and Chris Lasch. It described several generative, parametric systems. One of the recipes is shown here, cracking.

In this post we’ll build a generative system in Processing to explore the idea. The end result is: https://winterbloed.be/code/triangle.zip.

Except for the initial shape, everything seems to be made up of triangles. Remember from Li(n)e, Processing has a command line() but has no idea what a line is. Same goes for triangle(). We need to add the triangle concept ourselves. Taking the usual approach:

Starting off

In this case we decide to start from an equilateral triangle.

For that we need the coordinates of the corner points. With some looking around we’ll undoubtedly find these online but let’s take a different approach.

What is this p1.rotate()? Imagine that we create the first point p1 of our triangle vertically at some distance radius from the origin, (0, radius). If we rotate this point around the origin by 120° we end up at the second point, another 120° takes us to the third point. A third rotation by 120° gives us a full turn and we get back to the first point.

Rotating a point around the origin in 2D is something that might have come up in our math curriculum, definitely something with sines and cosines. Let’s look it up, nobody expects us to know this from heart (now). With some digging, and ignoring ominous looking matrix notations and trigonometric derivations, diagonally scanning Wikipedia and Stack Overflow we find something that looks like the right thing.

x_{rotated}=x.cos(angle)-y.sin(angle)

y_{rotated}=x.sin(angle)+y.cos(angle)

Let’s build that into our Point.

Calling p1.rotate(angle) will return a new Point, p1 rotated about the origin over the specified angle. Trigonometry functions expect their angles to be in radians, but sometimes it’s easier for us to think in degrees. So rather than p1.rotate(2*PI/3.0), I prefer p1.rotate(radians(120)).

That will give us our starting triangle. As our cracking progresses, we’ll need a list to store our ever-growing collection of new triangles. In the beginning that list is empty except for the single triangle start.

Subdivision

Writing an iterative subdivision sketch for the first time can be confusing. After all, we’re starting with a single triangle, after a first division we’ll have several more. In the second iteration we want to subdivide those new triangles but not the first, and so on.

My favorite method is like this:

Key feature is that while we are going through triangles, we’re not adding anything to it. In fact, this would result in an error, ConcurrentModificationException. Modifying a list we’re iterating over is rarely a good idea anyway. Especially when adding things to the end of the list it’s easy to create a piece of code that will never end. We don’t have enough time for infinity.

We solve this by storing the new subdivided triangles in a fresh new list every iteration. Once we finish going through triangles, we discard it by overwriting it with the new list subTriangles. That way we go into the new round with only the latest generation of subdivisions.

We skipped over the triangle subdivision itself. The recipe in Tooling calls for creating a new triangle by connecting every side with a center point inside the shape. We add a function to our Triangle that returns a point inside, in this case the average of the corners, the centroid:

We add everything together and we have the first version of our iterative subdivision, cracking sketch.

Great! Two things of note. One, iterative subdivision grows very quickly. Here every triangle is replaced by 3 triangles, so the total number is multiplied by 3 every cycle. 1, 3, 9, 27, … after only 10 iterations we’re almost up to 60000, 20 iterations and we’re at 3.5 billion. It’s not a bad idea to save ourselves a forceful exit by limiting the number of iterations.

Secondly, every time we run the code, the result will be identical. Time to add some variety.

Variations

One way is to create a random point inside the triangle rather than use the centroid. For example, we can generalize our current equation

p=({\frac 1 3}x_{p1} + {\frac 1 3}x_{p2} + {\frac 1 3}x_{p3}, {\frac 1 3}y_{p1} + {\frac 1 3}y_{p2}+ {\frac 1 3}y_{p3})

to

p=({a}x_{p1} + {b}x_{p2} + {c}x_{p3}, {a}y_{p1} + {b}y_{p2} + {c}y_{p3})

with a,b and c random positive numbers (or zero). An additional condition is that their sum should be 1. Where does this come from? It’s not something that I would likely come up with myself. But when snooping around for something completely else, I came across barycentric coordinates, a way of describing the position of a point in a triangle in relation to its three corner points. Turns out that for every point inside a triangle we can find exactly one unique a,b and c combination, and that their sum is always 1. Conversely, if we make up such a combination that should give us a point in the triangle.

When I came across barycentric coordinates, I didn’t know we could use it for something like this. It was just one of the many odds and ends we picked up along the way. Creative coding is McGyvering our way through the challenges.

Instead of fixing the centroid, we’re generating random pairs of a and b. c is fixed by the condition that the sum has to be 1. If the sum of a and b turns out to be larger than 1, there’s no room left to create a positive c, so we re-roll until we find a suitable pair.

Definitely more random, but maybe too random? Is there a way to tune the amount of noise, to find some goldilocks zone of randomness? In this case I tackled it by considering that in our first version a, b and c are always 1/3. And in the second, fully random version, they’re some number between 0 and 1. What if we could mix these together. No randomness would give us 1/3, full randomness would give us the randomly generated value, and something in-between would interpolate.

This modification does just that.

pointInTriangle() now takes an additional parameter f, randomness. If this is 0, the rolled values don’t matter, a, b and c are always 1/3. If it is 1, the rolled values of a and b are kept. Anything in-between interpolates, with higher values adding more of the random factors into the result.

Take care not to use 1/3 instead of 1.0/3.0. Both 1 and 3 are integers and 1/3 will be interpreted as an integer division and will equal 0! 1.0/3 or 1/3.0 would work fine too.

RANDOMNESS 0.2, 7 iterations
RANDOMNESS 0.4, 7 iterations
RANDOMNESS 0.6, 7 iterations

More

Another way to introduce variation is to relax the rule that every triangle has to be subdivided every iteration. Maybe some can escape annihilation. We add an additional parameter CHANCE and tweak the subdivide() logic.

When going through the triangle list, each triangle gets a roll, when it’s lower than CHANCE, it’s divided and discarded. When it rolls higher, it is saved and allowed to stay another round. Since we’re throwing away the original triangles list at the end, we have to add the saved triangle to the subTriangles list. Otherwise, we’ll get holes where parts go missing – which can be interesting.

The entire thing (https://winterbloed.be/code/triangle.zip):