There’s a weird thing that happens when you are really into something, and you start to try and explain it to others: it gets really difficult to do so.
It makes so much sense to you in your own head, and yet, is a total challenge to communicate clearly. In fact, the more complex the thing is, the more complicated it gets to put it into words - that is unless, you are sharing it with folks who are also into it like you are, and share in the thing’s language.
In this article I’m tossing that concern entirely to the wind, and just getting into the nitty gritty of the code and math behind Iterative. This will be an unbridled dive, so some knowledge of math may be helpful. If you’re into JavaScript and math, this may be repetitive, but I hope that it is at least informative. This will all hopefully be of interest to devs.
For those folks that have been along for the ride so far, you have an idea of what the art looks like. The Sandbox airdrops for IMP holders contain almost all of the fundamental code that will be in the primary collection of 1111 Iterations.
So what’s actually going on under the hood? Let’s start by dissecting the math and code in the latest airdrop, sandbox_drive.
In the fractalmath()
function I am using the Mandelbrot set algorithm to generate 3D fractals. One of the most important things to note here is that I am converting Cartesian coordinates into spherical coordinates to achieve 3D outputs using webGL (more on this later). These shapes are generally known as Mandelbulbs.
First, we declare a few integral set variables:
const maxIterations = 66;
const order = 6;
const inc = map(ethPrice, 1800, 6000, 0.07, 0.045);
const bail = 66;
maxIterations
determines the maximum number of iterations for the fractal to loop through. In the context of fractal generation, this value controls how deep the algorithm goes before stopping and determining the detail of the fractal. While higher numbers can imply more detail, some variables can negate some of the details. For example, low bail values can cause the algorithm to terminate before all additional iterations can be revealed (this will be very important at a later date for this project, since we are creating modifiable iterations…).
order
refers to the degree of the equation used in generating a fractal. Another way of wording this would be to say that it effects the shape and complexity of the final geometric object being created. Especially with Mandelbulbs, this value will determine how wildly different the resultant shapes we are rendering can be.
inc
stands for increment, which does exactly what the word means; its value determines the level of detail the fractal will have visually. Think of it as a granularity modifier - as we step through the coordinate space of the fractal, the inc value defines the distance between each point along the way. So if the number is lower, we get multiple points between A → B. If the number is higher, then we get less points between A → B. Lower value = more points in the rendered point cloud.
bail
is important here, because we want the fractal generation to literally bail out after a certain point, since fractal equations would essentially keep looping to infinity if we let them. This value essentially turns our math into what is known as an escape-time fractal algorithm, which means the fractal shape has a boundary. (We also don’t want to turn your silicon into a toaster).
Next, we nest three for
loops, over a 3D grid (with x0
, y0
, and z0)
:
for (let z0 = -1; z0 <= 1; z0 += inc) {
for (let x0 = -1; x0 <= 1; x0 += inc) {
let first = true;
let last = false;
for (let y0 = -1; y0 <= 1; y0 += inc) {
let x = x0;
let y = y0;
let z = z0;
let iteration = 0;
Within a range of -1 to 1, we increment with the set value of inc
. In doing so, we are having the setup scan for a cubic location, point by point by point. Booleans first
& last
act as flags to track beginning and ending positions later in the code. In the innermost loop here, x
, y
, and z
are initialized to current grid point values at x0
, y0
, and z0
, while iteration
is initialized to zero - all of which serves to prepare the starting point for the actual fractal calculations.
Now that we have set up the space to iterate through, we begin the computation of the fractal:
for (; ;) {
let xx = x * x;
let yy = y * y;
let zz = z * z;
let r = Math.sqrt(xx + yy + zz);
let theta = Math.atan2(Math.sqrt(xx + yy), z);
let phi = Math.atan2(y, x);
let powr = Math.pow(r, order);
let sinThetaOrder = Math.sin(theta * order);
let cosThetaOrder = Math.cos(theta * order);
We’re firing off an infinite loop with for (; ;)
, which essentially is the kick off for the computation, but also the thing we will have to break later with defined conditions, so that things don’t explode. This section here is also where we set up conversion from Cartesian to spherical coordinates. Wait, what the hell does that mean?!
OK, so for those new to, late to, or slept through trigonometry class, the Cartesian coord system represents points in space using orthogonal axes (x, y, z), while the spherical coord system uses a radius and two angles to describe a point's position relative to a central origin.
Here’s another way to express it: think of a grid on a flat map, and you use straight lines to show where any point on it is, by going across, and then up. WIth the spherical coord system, you’re inside a globe, and you draw straight lines from the center of it to a point, and then spin the entire globe vertically and horizontally to arrive at the desired location. Think of latitude and longitude on a map, but your first coordinate is somewhere along the depth of the earth’s layers. Visual aids below:
Now that that’s out of the way, let’s take a look at the math functions in the script that are computing the fractal by way of this conversion. (To get a basic understanding of the 2D Mandelbrot set algorithm, and the more complex Mandelbulb equations, be sure to look up the associated wiki pages, because there is a character limit on these articles and I’m not about to get into that specific math.)
Let’s convert the coordinates:
Using xx = x * x; yy = y * y; zz = z * z;
, we compute the square of each Cartesian coordinate.
Using r = Math.sqrt(xx + yy + zz);
we calculate the radial distance (), which is equivalent to the magnitude in spherical coordinates.
We then define theta
and phi
to convert the Cartesian coords to spherical angles * *(polar angle) and * *(azimuthal angle)
Let’s apply the Mandelbulb transformations:
This here powr = Math.pow(r, order);
raises the radial distance to the power of order
, which we defined earlier. This is analogous with the radial element of the Mandelbulb equation.
And then we set up some shorthand for sin(theta * order)
and cos(theta * order);
which we will use to apply the Mandelbulb transformations to the angles. Do note here that we are using order
again, to scale the angles and (polar and azimuthal), and i’m not actually using the shorthand variables in this specific sketch (though they are used quite commonly throughout most sketches in the collection).
So that’s nice, we can create basic Mandelbulbs now. However, Iterative is not about exploring the basics; it’s about a spatial journey into discovering new shapes, and iterating through variations of them. Variations on variations on variations… In this particular iteration, we have this math here (using 2500 as the price of eth to simplify the blob):
x = powr * Math.pow(2500, 3) * Math.sin(order * theta + Math.PI / 2) * Math.cos(theta) * random(1, 2500);
y = powr * Math.pow(2500, 4) * Math.sin(order * phi + Math.PI / 2) * Math.sin(phi) * random(1, 2500);
z = powr * Math.pow(2500, 5) * Math.sin(order * (theta + phi)) * Math.tan(theta + phi) * random(1, 2500);
Here, the pow()
function is raising the value to a power of 3, which will exponentially scale the dimensions of the fractal, which also means its complexity also increases.
We are also multiplying powr
(the radius raised to the order
) with the above value, before applying the trigonometric transforms with theta
and pi
. This essentially results in a pretty wacky warping of the fractal along these pre-defined axes.
The increment in power (pow()
) across x, y, and z, ensures that we end up with a non-symmetrical Mandelbulb. z
is also a little different because it’s combing theta
and phi
into a single trigonometric term, which makes a really weird interaction occur between the two angles.
I did add a random()
factor at the end of these in this specific iteration, as I want the price of Eth to really distort the f*ck out of the fractal shapes in non-uniform ways.
Across the 30 different types of iterations I have created, there are 30 different core variations occurring, although there are a number of iterations that are non-fractals, that still observe similar trigonometric rules, and a number of hybrids, that mesh the two together. Almost all of the 30 types are very different.
Next up, we have the algorithm perform iterations and some very important checks for conditions, in order to establish points belonging to the fractal we are creating.
x += x0;
y += y0;
z += z0;
iteration++;
const p = new createVector(x0, y0, z0);
if (iteration > maxIterations) {
if (first) {
mandelPoints.push(p);
first = false;
last = true;
}
break;
}
if (xx + yy + zz > bail) {
if (last) {
mandelPoints.push(p);
last = false;
first = true;
}
Following the previous equations that are doing all the shape discovery, we now proceed to update the coords for x
, y
, and z
by adding the original grid points at x0
, y0
, and z0
. This repositions the transformed point back relative to its original grid location. From here, we can increment the iteration counts for each loop with iteration++
, which tracks how many times the loop has run for the current point. And to make sure we know which way things are going, we create a vector with p = new createVector(x0, y0, z0);
, wherein p is the current grid point in 3D space.
Now that we’ve adjusted the position, we can move on to checking conditions we defined earlier. We take a look at whether the iteration count has exceeded the max value we set earlier (maxIterations
) with if (iteration > maxIerations)'
. And if it has, and it’s the first iteration that has done so (`first = true`
), we add p
to the mandelPoints
array we are creating. Yep, our first
and last
flags are finally getting updated, and we’re setting some boundaries.
Now we’ve also gone and created mandelPoints
here, which is very important in many regards. Before I explain what that does, let’s wrap up the condition checks. The second condition check with if (xx + yy + zz > bail)
checks if the squared distance from the origin exceeds bail
, the thing preventing everything from exploding. Now if the distance does indeed exceed the limit and the point we’ve just established is the last one after having done so (last = true
), it’s added to the mandelPoints
array, and the flags are updated accordingly.
At this point we break
to terminate the innermost loop so we can move the computation to the next point in the grid. All of this to draw a single point! Again, and again, and again, and…
And so, for the myriad of points we’re discovering across the shape we have created, mandelPoints
plays the absolutely crucial role of storing the points that we are establishing as being part of the Mandelbulb we have created. It is the algorithm’s storage unit, and all of its contents will help us draw the shape later on. In fact, we can use this array to do literally anything with the points it stores, from moving individually indexed points around, to changing what represents them (characters or primitives like spheres), as just a couple of examples. We are the captains now, of each point!
for (let v of mandelPoints) {
let scale = map(ethPrice, 2000, 6000, 750, 1200);
v.mult(scale);
}
for (let i = 0; i < mandelPoints.length; i++) {
mandelPoints[i].rotationSpeed = random(0.5, 2);
mandelPoints[i].rotationOffset = random(360);
mandelPoints[i].rotationDelay = frameCount + floor(random(0, 100));
}
}
In this final section of fractalmath()
, we take each vector v
in mandelPoints
, and scale them uniformly with a predefined scale
value. We’re essentially sizing each point in the fractal here so it can be big or small. In this case, we’re using the price of Ethereum to determine the scale.
Here, we are also passing along some animation properties for each point in mandelPoints
. A big part of this project is displaying the Mandelbulbs in motion, so that it’s not just trippy to look at, but so we can get a sense of spatiality; the dimension of time allows us to observe 3D dimensional space as it moves, allowing us to better grok what we’re perceiving visually. rotationSpeed
, rotationOffset1
, and rotationDelay
will all come in very handy later when we start having fun in draw()
.
In upcoming articles, I will touch on some of the functions around various features, how we’re drawing the shape with various embellishments, interactive elements, how some variables translate to traits in the metadata for the tokens they will be associated with, and more. I will also be sharing an article about the work Moon has been doing with the contracts behind Iterative, which is the other 50% of this project’s dev.
You can view sandbox_drive iterating live on Manifold, here:
And you can check out the code either in your browser’s inspector, or just check out the entire p5.js directly on Arweave here.
And of course, you can check out our website and find your way to our XthatisformerlyTwitterorwhatever or our Discord from there…
As a reminder, this indie project is entirely funded by Iterative Minting Pass sales leading to mint. Funds from IMP sales will help fund our contract array being deployed, as well as funding artists involved with the project, architecture enhancements, and more.
Our Iterative Minting Pass doubles as a presale purchase (you get one iteration airdropped from the main collection on day of mint), and triples as a perks booster. By holding an IMP, you get airdrops on the regular from our Sandbox creations, as well as having a say in the direction of the project.You can pick up your own IMP here: