Mandelbrot Set in MAXScript

The Mandelbrot Set (MAXScript Fractal)

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++.

Mandelbrot Set in MAXScript (Zoom Animation)

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:

M = \begin{Bmatrix}  c \in \mathbb{C} \mid \lim_{n \to \infty} Z_n \neq \infty  \end{Bmatrix}

where:

  • Z_0 = c
  • Z_{n+1} = Z_n^2 + c

 

The Mandelbrot Set, Expressed Programmatically

Within a computer graphics context, c    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, Z  .

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

But wait – we already have a problem! Z  cannot possibly iterate an infinite number of times, and further, nor can a CPU know if Z  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 Z  . To remedy this, we assume that after an arbitrary number of iterations, if Z   has an absolute value greater than 2, it will indeed escape to infinity.

Each time that Z  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 c   , one of two things can happen:

  • The iteration count reaches its limit and Z  still evaluates to a finite value
    • (i.e. c \in \mathbb{C}  )
  • Z  crosses the infinity threshold before the iteration limit is reached
    • (i.e. c \notin \mathbb{C}  )

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 c   , 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:

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!

Mandelbrot - MAXScript Fractal (Initial Setup)

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. Z  is implemented by a while loop, and c   pretends to be a complex number, but is actually just the coordinates for the current cube being fed through the algorithm.

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.

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 c   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.

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.

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:

ws_x \equiv i \mod{res_x}

A clock is a good analogue of modulus arithmetic. Our best estimates place the big bang as having occurred 1.2 \times 10^{14}  hours ago. Yet, if you glance over at the clock on the wall it does not read: quarter past 1.2 \times 10^{14}  hours. This is because analogue clocks are \mod{12}  . When they get to 12, they loop back around to 0 (i.e. 0 \equiv 12 \mod{12}  ). In fact, when you read an analogue clock, your brain is calculating \mod{12}  (for hours) and \mod{60}   (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:

1 \equiv i \mod{res_x}

where:

  • i = res_x + 1

This effectively defines our columns.

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

ws_y = | i / res_x |

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. | 4.36 | = 4  ). (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 Z   .

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 c   . Meaning:

c = it_x + it_y

where:

  • c    is a complex number
  • it_x   is a real number (or the x-axis of a complex plane)
  • it_y    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 Z  , and iterationMax, the iteration threshold at which we deem Z  for a given value of c   to be finite (if less than 2 \times 2  ).

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

Mandelbrot Set in MAXScript (Algorithm Iteration Animation)

Let’s move on to Z  .

Z  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, c   (formed of it_x + it_y  ) cannot have an absolute value (or magnitude) exceeding 2.

The second condition imposes a limit to the number of times Z  can recur. If a permutation of c   survives until the last recursion of Z  c   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.

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

Z_{n+1} = Z_n^2 + c

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.

If we are to colour the cubes based upon the value of iterationCount, a problem arises. After Z  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 Z  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 \frac{1}{32}   of iterationMax. This stops the cubes that escaped to infinity really quickly from being drawn.

Here are the statements for the if condition.

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.