top of page

NOC Week 8: Fractals!

Since I'm currently learning Maya, I thought it would be a nice challenge to render recursive objects in Maya using Maya's Embedded Language (MEL). I used a few tutorials and introductions to MEL in order to get the basics down to create my first object, which is like a Koch snowflake but with cubes. Knowing P5.js definitely helped my understanding of MEL, but I still had trouble with the syntax. I mean, what's the deal with all those dollar signs?? Anyway, here's my first script:

proc generateFractal(vector $center, float $size, int $recursionLevel)
   $recursionLevel -= 1;
   if($recursionLevel < 0)

// limits recursion level

polyCube -ch on -o on -w $size -h $size -d $size -cuv 4; //create cube

move -r ($center.x) ($center.y) ($center.z); //moves cube
//maya auto-selects last object created in scene (no selection code //necessary)

float $nextSize = $size/2.0; //next cube size
float $offset = $size/4.0 + $nextSize/2.0; // next cube offset

vector $right = <<($center.x + $offset), $center.y, $center.z>>; 
vector $left = <<($center.x - $offset), $center.y, $center.z>>; 
vector $front = <<$center.x, ($center.y + $offset), $center.z>>; 
vector $back = <<$center.x, ($center.y - $offset), $center.z>>;
vector $top = <<$center.x, $center.y, ($center.z + $offset)>>; 
vector $bottom = <<$center.x, $center.y, ($center.z - $offset)>>;

generateFractal($right, $nextSize, $recursionLevel); 
generateFractal($left, $nextSize, $recursionLevel); 
generateFractal($front, $nextSize, $recursionLevel); 
generateFractal($back, $nextSize, $recursionLevel); 
generateFractal($top, $nextSize, $recursionLevel); 
generateFractal($bottom, $nextSize, $recursionLevel); 

string $cubes[] = `ls "pCube*"`; 
select $cubes; 

vector $startPt = <<0,0,0>>; //starting cube parameters
float $startSize = 100.0;
int $startRecursionLevel = 6.0;

generateFractal($startPt, $startSize, $startRecursionLevel);
delete "pCube1";

So after all that, this is the result:

I then decided that I wanted to animate it. The shader that I created for the render above took 10 minutes to render a single frame (due to all of the light-refraction calculations), so I creating something a bit simpler and got the render time down to just below 2 minutes per frame. Below is the result.

I was also having issues generating recursion levels higher than 6 before Maya would crash. The render above was made of 1,555 cubes, and I think Maya struggled to add more objects to the scene. While this was exciting and a lot of fun to create, I wanted to do something a bit more complex, so I started looking at other fractals to see what else I could do.

I then stumbled on Mandelbulbs, which are a 3D version of the Mandelbrot set, and I they look really cool, so I wanted to try to make one of those in Maya. Luckily, Autodesk already has extensive documentation for creating Mandelbulbs and provided the code I needed to get started. The first thing I had to do was install the Arnold Procedurals plugin. Unfortunately this plugin is only compatible with Maya 2020 and Arnold Core version 6.1 and the mandelbulb.dll provided on the website will need to be altered for future versions of the Arnold - something I do not know how to do.

Anyway, once I got it running and generated my first Mandelbulb, I had to try to figure out how to animate it. The procedurals plugin allows for some alterations to the Mandelbulb. Playing with the grid size, power and max iterations yielded the most interesting changes to the bulb and all of the options

 name /obj/MandelJulia_Procedural1
 shader "/shop/standard1"
 grid_size 1600
 max_iterations 10
 power 8
 sphere_scale 1
 orbit_threshold 0.05
 chunks 30
 threads 16
 julia off
 julia_C -0.161224 1.04 0.183673

Above is the .ass (Arnold Scene Source) script that tells Maya what the customizable attribute values are. This is great, but we need a slightly different value for each frame if we want to make the Mandelbulb move...or wiggle. The best solution is to write a script that duplicates and iterates on this file over however many frames of animation we need. We can then load the .ass files as a sequence to correspond with the number of frames in the animation.

This I could not figure out how to do, but I amazingly after some googling, I found this artist CGStirk on ArtStation who had done just that, and posted a tutorial on how he created this script. He used PyMEL to create this script but it could have been written it in MEL as well.

He also took it one step further and customized the Mandelbulb dll to add a few more parameters to the source code which include phase shifting and XYZ offsets, along with other types of Mandelbulb formulae such as quadradic, quartic and quintic. I was happy enough with the phase shifting so I only added the code for the offset and phase-shifting variables.

Here's the chunk of the source code that I altered to add the offset and phase-shift parameter along with the necessary adjustment to the Mandelbulb equation:

// procedural parameters
struct Mandelbulb
    // General parameters
 int      grid_size;       // sample grid resolution
 int      max_iterations;  // max Z^n+C iterations to try
 float    sphere_scale;    // scales the spheres
 float    orbit_threshold; // clears out a hollow center in the set
 int      chunks;          // number of "chunks" for RAM management
 int      threads;         // number of threads to use
    AtVector offsetXYZ;       // adds an offset
 bool     julia;           // mandelbrot/julia set switch
    AtVector julia_C;         // C value for julia sets (unused for mandelbrot)

    // Standard mandelbulb parameters
 bool     is_standardMandelbulb; // use the standard mandelbulb (NOTE: if julia is also checked, it will only apply to the standard Mandelbulb)
 bool     approxR_N;        // approximates r^n for the standard mandelbulb
 float    power;           // exponent, the "n" in Z^n+C
 float    phase;           // adds a phase shift

 int counter;

// returns Z^n + C
// for more info:
static AtVector iterateMandelbulb(AtVector Z, float n, AtVector offset, float phase, bool approxR_N, AtVector julia_c)
    AtVector Zsquared;
 float r2 = AiV3Dot(Z, Z);
 float theta = atan2f(sqrtf(Z.x * Z.x + Z.y * Z.y), Z.z) + phase; //added**
 float phi = atan2f(Z.y, Z.x);
 float r_n;
 if (approxR_N)
        r_n = r2 * r2;
        r_n = powf(r2, n * .5f);
 const float sin_thetan = sinf(theta * n);

 Zsquared.x = r_n * sin_thetan * cosf(phi * n) + offset.x;
 Zsquared.y = r_n * sin_thetan * sinf(phi * n) + offset.y; 
 Zsquared.z = r_n * cosf(theta * n) + offset.z;
 return Zsquared + julia_c;


   // General parameters
 AiParameterInt("grid_size"                   , 400);
 AiParameterInt("max_iterations"              , 10);
 AiParameterFlt("power"                       , 8);
 AiParameterFlt("sphere_scale"                , 1);
 AiParameterFlt("orbit_threshold"             , 0.05);
 AiParameterInt("chunks"                      , 30);
 AiParameterInt("threads"                     , 16);
 AiParameterBool("julia"                      , false);
 AiParameterVec("julia_C"                     , 0, 0, 0);

   // Standard mandelbulb parameters
 AiParameterBool("is_standardMandelbulb"      , true);
 AiParameterBool("approx_mandelbulb"          , false);
 AiParameterFlt("phase"                       , 0);
 AiParameterVec("offsetXYZ"                   , 0, 0, 0); 

   Mandelbulb * bulb = new Mandelbulb();
   *user_ptr = bulb;

 bulb->grid_size = AiNodeGetInt(node, "grid_size");
 bulb->max_iterations = AiNodeGetInt(node, "max_iterations");
 bulb->sphere_scale = AiNodeGetFlt(node, "sphere_scale");  
 bulb->orbit_threshold = AiSqr(AiNodeGetFlt(node, "orbit_threshold"));
 bulb->chunks = AiNodeGetInt(node, "chunks");

 if (bulb->chunks < 2) { // needs to be at least 2 in order to check for empty scenes
 bulb->chunks = 2; // default chunks
 AiMsgInfo("[mandelbulb] Chunk size was less than 2. Setting chunks to %d ", bulb->chunks);

 bulb->threads = AiClamp(AiNodeGetInt(node, "threads"), 1, AI_MAX_THREADS);
 bulb->julia = AiNodeGetBool(node, "julia");
 bulb->julia_C = AiNodeGetVec(node, "julia_C");

 bulb->is_standardMandelbulb = AiNodeGetBool(node, "is_standardMandelbulb");
 bulb->approxR_N = AiNodeGetBool(node, "approx_mandelbulb");
 bulb->power = AiNodeGetFlt(node, "power");
 bulb->phase = AiNodeGetFlt(node, "phase");
 bulb->offsetXYZ = AiNodeGetVec(node, "offsetXYZ");
 bulb->counter = 0;
 return true;

Ok! So with that all done. Here are a few animations I was able to create using these custom attributes. The y-axis offset adds a really cool visual effect as it makes the Mandelbulb appear to grow out of the bottom of the screen. These were all rendered in the standard Arnold texture purely due to rendering constraints with my current system. Unfortunately any cool effects I could have made with different materials was taking too long to render. Overall, I think the monochromatic look is quite nice, even if it's not ideal. I love the idea of using the Mandelbulb to create some sort of animation with characters living on it's surface, maybe that's something I can work towards but for now I'm just excited to be entering the world of procedural animation in Maya (even if I'm just getting my toes wet).

I also have one more animation I'm currently rendering that will go at the end of this post. - stay tuned!


bottom of page