I decided to learn MAXScript by implementing the Mandelbrot set in 3DS Max. I was amazed by how easy a language it is. Datatypes are implicit in MAXScript, meaning that a variable’s datatype isn’t included in variable declarations. Although this makes the language more friendly to non-programmers, it is a bit strange when coming from C++.

Download the MAXScript fractal generator here: [wpdm_package id=’2067′]

## The Mandelbrot Set, Expressed Mathematically

The Mandelbrot set, despite its vast intricacy, can be boiled down to a very concise expression:

where:

## The Mandelbrot Set, Expressed Programmatically

Within a computer graphics context, can be thought of as the coordinates (in the complex plane) for any given pixel on the screen. The expression denotes the criteria for a complex number belonging to the Mandelbrot set, and includes the recursive function, .

takes as an argument, its result from the previous iteration. In order to be an element of the Mandelbrot set, must not escape to infinity.

But wait – we already have a problem! cannot possibly iterate an infinite number of times, and further, nor can a CPU know if escapes to infinity. Computers are finite-state machines. If we did not set some kind of cut-off point, the computer would never finish calculating . To remedy this, we assume that after an arbitrary number of iterations, if has an absolute value greater than 2, it will indeed escape to infinity.

Each time that does not cross our infinity threshold (i.e. absolute value of 2), the iteration count goes up by 1 until reaching an iteration limit. For each value of , one of two things can happen:

- The iteration count reaches its limit and still evaluates to a finite value
- (i.e. )

- crosses the infinity threshold before the iteration limit is reached
- (i.e. )

Another consideration is colouration. If our fractal is just a set of complex numbers, where do the colours arise from? The answer is very simple: iteration count. The greater the number of iterations for a given value of , the further the colour deviates from its starting point. If we wanted to map between black and white (in RGB colour-space), we could determine the colour of a pixel as such:

1 |
colour = iterationCount * (256/iterationMax) - 1 |

Great! We’ve overcome most of our hurdles, but one remains: 3DS Max is not a 2D rasterisation program! Well, given that 3DS Max is a 3D program, I thought it befitting to abstract pixels to cubes. Opting for planes was an option, but cubes better justify why on Earth I’m doing any of this in a 3D program!

## The Actual Code: a Walk-through

Here, we have the fundamental elements of the final script. I wrote some additional code to enable zooming and panning, but let’s not over-complicate things. I haven’t worried too much about optimisation. The code executes for every increment of 1 within a specified range, scanning left to right horizontally, before shifting up to a new, unscanned row. is implemented by a while loop, and pretends to be a complex number, but is actually just the coordinates for the current cube being fed through the algorithm.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
res_x = 225 res_y = res_x * 0.57142857142 res_xy = res_x * res_y infinityThreshold = 4 for i = 1 to res_xy do ( x_ws = mod i res_x y_ws = abs(i/res_x) x_md = x_ws * (3.5/res_x) - 2.5 y_md = y_ws * (2/res_y) - 1 x_it = 0 y_it = 0 iterationCount = 0 iterationMax = 200 while (x_it^2 + y_it^2 <= infinityThreshold and iterationCount < iterationMax) do ( x_temp = x_it^2 - y_it^2 + x_md y_it = 2 * x_it * y_it + y_md x_it = x_temp iterationCount += 1 ) if iterationCount != iterationMax and iterationCount > iterationMax/32 then ( createPos = [x_ws, y_ws, iterationCount * (0.75/iterationMax)] pBox = box pos: createPos length: 1 width: 1 height: 1 pBox.name = createPos as string colourLum = iterationCount * (256/iterationMax) - 1 colourHalf = colourLum/2 pBox.wirecolor = color colourLum colourHalf colourLum print createPos ) ) |

I have tried to keep things simple, but regardless, code can be pretty obnoxious to read when devoid of */* comments */. *Let’s remedy this by breaking the algorithm down into more intuitive chunks.

We begin with the following.

1 2 3 4 5 |
res_x = 225 res_y = res_x * 0.57142857142 res_xy = res_x * res_y infinityThreshold = 4 |

*res_x* is the width (i.e. *x* resolution) of the fractal, and *res_y* is the height (i.e. *y* resolution). Given that the Mandelbrot set resides in -2.5 to 1 on the x-axis, and -1 to -1, it has a width:height aspect ratio of 3.5:2. *res_y* is therefore derived from *res_x* and scaled to create the stated aspect ratio. *res_xy* is the area (or total number of cubes) of the fractal. infinityThreshold is the magnitude (or absolute value) that must surpass in order to be assumed escaping to infinity.

Now that we have some basic parameters set up, we need a way of examining each cube individually and applying the Mandelbrot set algorithm.

1 2 3 4 |
for i = 1 to res_xy do /* i.e. for each cube, do the following */ ( /* Cube placement and Mandelbrot calculations */ ) |

Everything significant is contained within this for loop. We are effectively saying: for each cube, starting at the first and looping until the last, do something. With each new loop iteration, 1 is added to *i *until* i *is equal to *res_xy* (i.e. the last cube)*.*

Next, we need to arrange our cubes into a two dimensional grid. *i*, adopting only a single quantity at a time, is just a linear sequence. A little arithmetic is required to break *i* into *x* and *y* coordinates.

1 2 3 4 5 6 7 8 |
for i = 1 to res_xy do ( x_ws = mod i res_x y_ws = abs(i/res_x) x_md = x_ws * (3.5/res_x) - 2.5 y_md = y_ws * (2/res_y) - 1 ) |

*x_ws* and *y_ws* stand for *x* worldspace, and *y* wordspace respectively. These two coordinates represent the final *xy *coordinates for each cube. mod *i* *res_x* looks nicer in mathematical notation:

A clock is a good analogue of modulus arithmetic. Our best estimates place the big bang as having occurred hours ago. Yet, if you glance over at the clock on the wall it does not read: quarter past hours. This is because analogue clocks are . When they get to 12, they loop back around to 0 (i.e. ). In fact, when you read an analogue clock, your brain is calculating (for hours) and (for minutes) at the same time.

Imagine a clock with 1 as its first hour and *res_x* as its last. Throughout the for loop iterations, i ascends normally up the number line, but upon reaching *res_x*,* i *goes back to zero such that:

where:

This effectively defines our columns.

The next line down, abs(*i*/*res_x*), deals with the rows. Again, mathematical notation is clearer:

abs() in the code and the vertical bars in the formula are equivalent. They both indicate their constituent is an absolute value, which is the non-decimal part of a number (i.e. ). (This type of absolute value is not to be confused with the other, denoting the distance of an expression from 0.) The line dictates that for each cycle between 1 and *res_x*,* y_ws *should increase by 1 increment.

The last two lines simply transform our worldspace coordinates such that they lie in the Mandelbrot range (x-axis range: -2.5 to 1, y-axis range: -1 to 1, as discussed earlier). *x_md* and *y_md* stand for *x* mandelbrot, and *y* mandelbrot respectively.

In summary, the algorithm thus far establishes a relationship between the fractal resolution, the current cube number, and the current cube coordinates (in worldspace and the Mandelbrot range). Any value can be fed into *res_x*, and the algorithm will construct a 2D array of cube coordinates with a width:height aspect ratio of 3.5:2. As the for loop executes, the algorithm scans from left to right until it has iterated *res_x* times. Upon this happening, it moves up one row and continues scanning until *res_x* is again reached. This continues until the for loop has iterated *res_xy* times (i.e. all cubes have been assigned coordinates).

Next, we want to declare and initialise four more variables before codifying .

1 2 3 4 5 |
x_it = 0 y_it = 0 iterationCount = 0 iterationMax = 200 |

First, are the two variables, *x_it* and *y_it*. These will be the real and imaginary parts of our complex number. The sum of the two numbers is . Meaning:

where:

- is a complex number
- is a real number (or the x-axis of a complex plane)
- is an imaginary number (or the y-axis of a complex plane)

I describe *x_it* and *y_it* as residing on the complex plane, but this is analogous to the XY worldspace coordinates of 3DS Max. We are effectively pretending 3DS Max’s worldspace contains a complex plane (on which the Mandelbrot set resides).

The second variable duo is immensely straightforward. *iterationCount* will indicate the current iteration of , and *iterationMax*, the iteration threshold at which we deem for a given value of to be finite (if less than ).

Below, we can see the effect of raising the maximum iteration count.

Let’s move on to .

1 2 3 4 |
while (x_it^2 + y_it^2 <= infinityThreshold and iterationCount < iterationMax) do ( /* Assignment of new values for x_it and y_it */ ) |

is encapsulated in a while loop. In order for the while loop to recur, two conditions must be met.

The first condition states the sum of the squares of *x_it* and *y_it* must not exceed the square of 2 (i.e. the infinity threshold). Put more intuitively, (formed of ) cannot have an absolute value (or magnitude) exceeding 2.

The second condition imposes a limit to the number of times can recur. If a permutation of survives until the last recursion of , is an element of the Mandelbrot set.

Recalling from earlier, these two conditions are set up to accommodate for the fact a computer cannot compute infinite quantities, nor purposefully iterate until infinity.

1 2 3 4 5 6 7 8 |
while (x_it^2 + y_it^2 <= infinityThreshold and iterationCount < iterationMax) do ( x_temp = x_it^2 - y_it^2 + x_md y_it = 2 * x_it * y_it + y_md x_it = x_temp iterationCount += 1 ) |

The statements contained within the while loop constitute the programmatic implementation of:

*x_temp* is created such that *x_it* retains its value until after *y_it* has been assigned a new value. *iterationCount *is increased by 1 increment, allowing the loop to keep track of which iteration it is on.

We have reached the final block of code.

1 2 3 4 |
if iterationCount != iterationMax and iterationCount > iterationMax/32 then /* Conditions for drawing a cube */ ( /* Instantiation and colourisation of current cube */ ) |

If we are to colour the cubes based upon the value of *iterationCount*, a problem arises. After has broken from its loop, members of the Mandelbrot set have an iterationCount equal to the iterationMax. Their colour would therefore be homogeneous! This makes the boxes not belonging to the Mandelbrot set far more interesting. Breaking with convention, we can save system resources by not drawing member boxes at all. Non-member boxes will be drawn instead.

The condition, *iterationCount* != *iterationMax* ensures that exceeded the escape-to-infinity value (*infinityThreshold*) before breaking. The second condition, iterationCount > iterationMax/32 is a further optimisation I have made. In plain English, the condition is stating: do not execute for a cube with an *iterationCount* below of *iterationMax*. This stops the cubes that escaped to infinity really quickly from being drawn.

Here are the statements for the if condition.

1 2 3 4 5 6 7 8 9 10 |
if iterationCount != iterationMax and iterationCount > iterationMax/32 then ( createPos = [x_ws, y_ws, iterationCount * (0.75/iterationMax)] pBox = box pos: createPos length: 1 width: 1 height: 1 pBox.name = createPos as string colourLum = iterationCount * (256/iterationMax) - 1 colourHalf = colourLum/2 pBox.wirecolor = color colourLum colourHalf colourLum ) |

*createPos* is a 3vector storing the worldspace location of the current box. The z coordinate of *createPos *is* *tied to *iterationCount* such that values almost belonging to the Mandelbrot set ascend upward. Next, we declare and initialise *pBox*, and in doing so, instantiate a cube positioned at *createPos*. Interestingly, MAXScript is an object-orientated language. *pBox *then adopts its position (*createPos*) as its name (*pBox.name*).

*colourLum* stands for colour luminosity and maps the minimum and maximum iterationCount values to between 0 and 255. This is the range (per channel) of 8-bit RGB colour space. *colourHalf* should be self explanatory.

The very last line of code assigns pBox a colour based upon *colourLum* and *ColourHalf*, biasing in favor of the red and blue channels. Thus, the outputted fractal is accorded a violet hue.

That’s it!

Thanks for reading.